summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Data/Sys/ApprovedInis.json2
-rw-r--r--Data/Sys/GameSettings/G3L.ini2
-rw-r--r--Data/Sys/GameSettings/G4S.ini2
-rw-r--r--Data/Sys/GameSettings/G4SP01.ini2
-rw-r--r--Data/Sys/GameSettings/GEYJ6E.ini4
-rw-r--r--Data/Sys/GameSettings/GEYP6E.ini4
-rw-r--r--Data/Sys/GameSettings/GGGE6E.ini5
-rw-r--r--Data/Sys/GameSettings/GHAE6E.ini5
-rw-r--r--Data/Sys/GameSettings/GHZW6E.ini4
-rw-r--r--Data/Sys/GameSettings/GJAP6E.ini4
-rw-r--r--Data/Sys/GameSettings/GLKJ6E.ini4
-rw-r--r--Data/Sys/GameSettings/GLLP6E.ini4
-rw-r--r--Data/Sys/GameSettings/GWWE01.ini2
-rw-r--r--Data/Sys/GameSettings/GWWJ01.ini2
-rw-r--r--Data/Sys/GameSettings/GWWP01.ini2
-rw-r--r--Data/Sys/GameSettings/GXRP08.ini6
-rw-r--r--Data/Sys/GameSettings/RMC.ini1
-rw-r--r--Data/Sys/GameSettings/RSM.ini3
-rw-r--r--Data/Sys/GameSettings/RUUE01r0.ini254
-rw-r--r--Data/Sys/GameSettings/RUUE01r1.ini225
-rw-r--r--Data/Sys/GameSettings/RUUJ01r1.ini225
-rw-r--r--Data/Sys/GameSettings/RUUK01r1.ini225
-rw-r--r--Data/Sys/GameSettings/RUUP01r0.ini233
-rw-r--r--Data/Sys/GameSettings/RUUP01r1.ini233
-rw-r--r--Data/Sys/GameSettings/RZD.ini1
-rw-r--r--Data/Sys/GameSettings/SMB.ini2
-rw-r--r--Externals/licenses.md2
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.kt14
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt6
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt16
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlay.kt8
-rw-r--r--Source/Android/app/src/main/res/values/arrays.xml4
-rw-r--r--Source/Android/app/src/main/res/values/strings.xml3
-rw-r--r--Source/Android/jni/Config/NativeConfig.cpp5
-rw-r--r--Source/Core/Common/BitUtils.h4
-rw-r--r--Source/Core/Common/ChunkFile.h2
-rw-r--r--Source/Core/Common/CommonPaths.h3
-rw-r--r--Source/Core/Common/FileUtil.cpp1
-rw-r--r--Source/Core/Common/FileUtil.h3
-rw-r--r--Source/Core/Common/Logging/Log.h5
-rw-r--r--Source/Core/Common/Logging/LogManager.cpp5
-rw-r--r--Source/Core/Common/Network.cpp229
-rw-r--r--Source/Core/Common/Network.h39
-rw-r--r--Source/Core/Common/StringUtil.h27
-rw-r--r--Source/Core/Common/UnixUtil.h6
-rw-r--r--Source/Core/Core/AchievementManager.h4
-rw-r--r--Source/Core/Core/Boot/Boot.cpp19
-rw-r--r--Source/Core/Core/Boot/Boot_BS2Emu.cpp11
-rw-r--r--Source/Core/Core/BootManager.cpp76
-rw-r--r--Source/Core/Core/CMakeLists.txt10
-rw-r--r--Source/Core/Core/CheatSearch.cpp9
-rw-r--r--Source/Core/Core/CheatSearch.h4
-rw-r--r--Source/Core/Core/Config/MainSettings.cpp58
-rw-r--r--Source/Core/Core/Config/MainSettings.h2
-rw-r--r--Source/Core/Core/ConfigLoaders/BaseConfigLoader.cpp64
-rw-r--r--Source/Core/Core/ConfigLoaders/BaseConfigLoader.h16
-rw-r--r--Source/Core/Core/ConfigManager.cpp30
-rw-r--r--Source/Core/Core/ConfigManager.h6
-rw-r--r--Source/Core/Core/Debugger/BranchWatch.h29
-rw-r--r--Source/Core/Core/HW/DVD/AMMediaboard.cpp2180
-rw-r--r--Source/Core/Core/HW/DVD/AMMediaboard.h274
-rw-r--r--Source/Core/Core/HW/DVD/DVDInterface.cpp55
-rw-r--r--Source/Core/Core/HW/EXI/BBA/TAPServerConnection.cpp12
-rw-r--r--Source/Core/Core/HW/EXI/EXI.cpp24
-rw-r--r--Source/Core/Core/HW/EXI/EXI.h1
-rw-r--r--Source/Core/Core/HW/EXI/EXI_Device.cpp2
-rw-r--r--Source/Core/Core/HW/EXI/EXI_DeviceBaseboard.cpp279
-rw-r--r--Source/Core/Core/HW/EXI/EXI_DeviceBaseboard.h44
-rw-r--r--Source/Core/Core/HW/GCPadEmu.cpp21
-rw-r--r--Source/Core/Core/HW/GCPadEmu.h10
-rw-r--r--Source/Core/Core/HW/MagCard/C1231BR.cpp82
-rw-r--r--Source/Core/Core/HW/MagCard/C1231BR.h37
-rw-r--r--Source/Core/Core/HW/MagCard/C1231LR.cpp27
-rw-r--r--Source/Core/Core/HW/MagCard/C1231LR.h26
-rw-r--r--Source/Core/Core/HW/MagCard/MagneticCardReader.cpp1000
-rw-r--r--Source/Core/Core/HW/MagCard/MagneticCardReader.h207
-rw-r--r--Source/Core/Core/HW/Memmap.h1
-rw-r--r--Source/Core/Core/HW/SI/SI.h1
-rw-r--r--Source/Core/Core/HW/SI/SI_Device.cpp4
-rw-r--r--Source/Core/Core/HW/SI/SI_Device.h2
-rw-r--r--Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.cpp2353
-rw-r--r--Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.h315
-rw-r--r--Source/Core/Core/HW/VideoInterface.cpp4
-rw-r--r--Source/Core/Core/IOS/Network/Socket.cpp6
-rw-r--r--Source/Core/Core/IOS/USB/Emulated/Microphone.h1
-rw-r--r--Source/Core/Core/NetPlayClient.cpp33
-rw-r--r--Source/Core/Core/PowerPC/BreakPoints.cpp14
-rw-r--r--Source/Core/Core/PowerPC/BreakPoints.h10
-rw-r--r--Source/Core/Core/PowerPC/Jit64/Jit.cpp5
-rw-r--r--Source/Core/Core/PowerPC/Jit64/Jit.h6
-rw-r--r--Source/Core/Core/PowerPC/Jit64/Jit_Branch.cpp171
-rw-r--r--Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp46
-rw-r--r--Source/Core/Core/PowerPC/Jit64/Jit_LoadStore.cpp19
-rw-r--r--Source/Core/Core/PowerPC/JitArm64/Jit.cpp9
-rw-r--r--Source/Core/Core/PowerPC/JitArm64/Jit.h4
-rw-r--r--Source/Core/Core/PowerPC/JitArm64/JitArm64_Branch.cpp146
-rw-r--r--Source/Core/Core/PowerPC/JitArm64/JitArm64_LoadStore.cpp23
-rw-r--r--Source/Core/Core/PowerPC/JitCommon/JitBase.h6
-rw-r--r--Source/Core/Core/PowerPC/PowerPC.cpp3
-rw-r--r--Source/Core/Core/State.cpp2
-rw-r--r--Source/Core/Core/TitleDatabase.cpp28
-rw-r--r--Source/Core/Core/TitleDatabase.h3
-rw-r--r--Source/Core/DiscIO/Blob.h3
-rw-r--r--Source/Core/DiscIO/CachedBlob.cpp8
-rw-r--r--Source/Core/DiscIO/Enums.cpp2
-rw-r--r--Source/Core/DiscIO/Enums.h9
-rw-r--r--Source/Core/DiscIO/Volume.h11
-rw-r--r--Source/Core/DiscIO/VolumeDisc.cpp98
-rw-r--r--Source/Core/DiscIO/VolumeDisc.h27
-rw-r--r--Source/Core/DiscIO/VolumeGC.cpp33
-rw-r--r--Source/Core/DiscIO/VolumeGC.h14
-rw-r--r--Source/Core/DolphinLib.props10
-rw-r--r--Source/Core/DolphinQt/CMakeLists.txt2
-rw-r--r--Source/Core/DolphinQt/CheatSearchWidget.cpp45
-rw-r--r--Source/Core/DolphinQt/CheatSearchWidget.h1
-rw-r--r--Source/Core/DolphinQt/Config/GamecubeControllersWidget.cpp4
-rw-r--r--Source/Core/DolphinQt/Config/InfoWidget.cpp6
-rw-r--r--Source/Core/DolphinQt/Config/Mapping/GCPadEmu.cpp16
-rw-r--r--Source/Core/DolphinQt/Config/Mapping/GCPadEmu.h20
-rw-r--r--Source/Core/DolphinQt/Config/Mapping/MappingWindow.cpp10
-rw-r--r--Source/Core/DolphinQt/Config/Mapping/MappingWindow.h2
-rw-r--r--Source/Core/DolphinQt/Config/SettingsWindow.cpp4
-rw-r--r--Source/Core/DolphinQt/Config/SettingsWindow.h1
-rw-r--r--Source/Core/DolphinQt/Debugger/BreakpointWidget.cpp55
-rw-r--r--Source/Core/DolphinQt/Debugger/BreakpointWidget.h2
-rw-r--r--Source/Core/DolphinQt/Debugger/CodeViewWidget.cpp3
-rw-r--r--Source/Core/DolphinQt/Debugger/NetworkWidget.cpp206
-rw-r--r--Source/Core/DolphinQt/Debugger/NetworkWidget.h7
-rw-r--r--Source/Core/DolphinQt/DolphinQt.vcxproj4
-rw-r--r--Source/Core/DolphinQt/MainWindow.cpp32
-rw-r--r--Source/Core/DolphinQt/MainWindow.h2
-rw-r--r--Source/Core/DolphinQt/MenuBar.cpp4
-rw-r--r--Source/Core/DolphinQt/MenuBar.h1
-rw-r--r--Source/Core/DolphinQt/Settings/AdvancedPane.cpp2
-rw-r--r--Source/Core/DolphinQt/Settings/GameCubePane.cpp61
-rw-r--r--Source/Core/DolphinQt/Settings/GameCubePane.h6
-rw-r--r--Source/Core/DolphinQt/Settings/TriforcePane.cpp221
-rw-r--r--Source/Core/DolphinQt/Settings/TriforcePane.h14
-rw-r--r--Source/Core/InputCommon/ControllerInterface/Android/Android.cpp65
-rw-r--r--Source/Core/InputCommon/GCPadStatus.h9
-rw-r--r--Source/Core/UICommon/GameFile.cpp5
-rw-r--r--Source/Core/UICommon/GameFile.h2
-rw-r--r--Source/Core/UICommon/GameFileCache.cpp2
-rw-r--r--Source/Core/UICommon/UICommon.cpp1
-rw-r--r--Source/UnitTests/Common/StringUtilTest.cpp150
145 files changed, 10142 insertions, 710 deletions
diff --git a/Data/Sys/ApprovedInis.json b/Data/Sys/ApprovedInis.json
index 78bba10e8d..34a3ce9c56 100644
--- a/Data/Sys/ApprovedInis.json
+++ b/Data/Sys/ApprovedInis.json
@@ -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":"478DA1D596D8513C343850719155813CB61A56DD"},"GCBP7D":{"16:9 Widescreen":"478DA1D596D8513C343850719155813CB61A56DD","60Hz":"DF2C4DDF082C9E67919443C5ACC485AB2CF7F8D0"},"GCCE01":{"16:9 Widescreen":"BA0066518E7EA8599A81F8496BD39DE20CC369D6","Fix GBA connections":"483BDB94615C690045C3759795AF13CE76552286","Fix buffer overrun bug (crash at Goblin Wall)":"6C107FEC15C76201233CA2645EB5FAB4FF9751CE"},"GCCJGC":{"Fix GBA connections":"4C104D24329172F5D0F8649DE9423B931FE72CA3"},"GCCP01":{"16:9 Widescreen":"2580E7379633CF4DE13B9EC945DB17543477828A","Fix GBA connections":"2EAA60A8A115AD68A795109FB59E4A726D29016D"},"GCN":{"Fix C4 texture tiling (used for buttons and some character icons)":"601FE183C9524ACCF068874DABD73921C86769CF"},"GCVEEB":{"16:9 Widescreen":"ADA686C90189D1D7D23E23E525EFC96EFA104BB9"},"GD7E70":{"Deinterlacing Fix":"6F8B6E33214162E5CDEBD2BC6A60FAFA04D2EB10"},"GD7PB2":{"Deinterlacing Fix":"3B798C9D63E755349B253FAF9186BAE1F703D96A"},"GDREAF":{"Fix audio issues":"F8EAE60FEB0CFB4477FDC4B9E136B63F68DFA63A"},"GDRP69":{"Fix audio issues":"E23D98B2CE185C3993A40F2495D37E41B971BF91"},"GEDE01":{"Fix startup hang":"21068C3CE905FB0CFFAA7408A93154AF8A5295A2"},"GEDJ01":{"Fix startup hang":"7061F3CF11BF64D3BA7F32CCF2BAC42FF3614AB6"},"GEDP01":{"Fix startup hang":"6F1B00517CBA30BEB738EAA90E71221378CD570D"},"GEME7F":{"Force Progressive Scan":"CB04E00918C9C0F161715D21D046ED6620F7ADEF"},"GEMJ28":{"Force Progressive Scan":"41D8F2C833C0B7B79FCA038752052059207AE4D5"},"GEZE8P":{"16:9 Widescreen [gamemasterplc]":"5CEBCFBEA4E444495D2FD6D8B6607DFB2349CC1B"},"GF7E01":{"16:9 Widescreen [gamemasterplc]":"C59CCA3EF8A5E97B32EB64DB9AE80E652ED281C6"},"GF7P01":{"16:9 Widescreen [gamemasterplc]":"1230053B5C347B62E432EFD6635433A183D18619"},"GFQEA4":{"16:9 Widescreen":"5CD9568CE58EF82EB371594667015C9B799454B9"},"GFZJ01":{"Make Save Copyable":"F659D22CB1DFF15C3915D7630D838EED8DB0BA48"},"GFZP01":{"Make Save Copyable":"F659D22CB1DFF15C3915D7630D838EED8DB0BA48"},"GGTE01":{"16:9 Widescreen [darkludx]":"EF128E7A9C22676834F558BA0F0F7FD8B9028727","Experimental 60fps":"D40344111D989EA009901F8B1C45B5AC8D39E6D2","Simple 60FPS":"5232A937D1D813FF58DD71D716284ED6AB535689"},"GGTJ01":{"Experimental 60fps":"BEC2310911003AF9E4B984A4051E0F885B8CC825","Simple 60FPS":"D62E777A2F019D7BD0AFFBCE876BD9AE408F1667"},"GGTP01":{"16:9 Widescreen":"FA9CD330ECDA01275DA88BD0803DE47757D16A4E","Experimental 60fps":"EB1ACD613BB432A3391CFFF1F0145A6ACE66B210","Simple 60FPS":"43FAD0DDD130BE1E4C8C7603EF6CA7DAA0DF5EE9"},"GGVD78":{"16:9 Widescreen":"9D07DBB5EC2FAA47F2E4587FBD75FD6F3E7E91B0","EFB Copy Fix":"FE52240DF6D132C15A8324E8A477F2BF2250D208"},"GGVE78":{"16:9 Widescreen":"86E561452235BF88D41884558EF34F54CE0FEB48","EFB Copy Fix":"5E38E10829D5F77243C95E9E41518BB3ADE24139"},"GGVP78":{"16:9 Widescreen":"F9B2108D833084FA36A53E00F1647A9579F847D3","EFB Copy Fix":"5E38E10829D5F77243C95E9E41518BB3ADE24139"},"GGVX78":{"EFB Copy Fix":"740F2D1C01DA39D1760D96B03974A48E6F74578D"},"GHAE08":{"Fix audio issues":"9799AFF8463EC86C9230E31E2627E141F0C129D3"},"GHAJ08":{"Fix audio issues":"B45A8FC32D14567B8D6C95F303E00A72C0E1D344"},"GHAP08":{"Fix audio issues":"BC7F3CFC97593AA2055C370C175950DC478D2709"},"GHKD7D":{"Limit internal frame rate (speed hack, also fixes audio and gameplay bugs)":"1AF09E98F0B72833A0F4E519EC3D4130C36D615E"},"GHKE7D":{"Limit internal frame rate (speed hack, also fixes audio and gameplay bugs)":"9BFBDE9184BC05EBD545625DEE486E3AAB36B6A3"},"GHKF7D":{"Limit internal frame rate (speed hack, also fixes audio and gameplay bugs)":"919E853513192C97F3CF49246A65ECBF39E7C201"},"GHKP7D":{"Limit internal frame rate (speed hack, also fixes audio and gameplay bugs)":"1AF09E98F0B72833A0F4E519EC3D4130C36D615E"},"GHKS7D":{"Limit internal frame rate (speed hack, also fixes audio and gameplay bugs)":"919E853513192C97F3CF49246A65ECBF39E7C201"},"GHLE69":{"16:9 Widescreen":"742BFC79D8F0BA5D6215772B58F5A0EADD7BFAFF"},"GHQE7D":{"16:9 Widescreen":"520E5F718D3BD1291C55021776091E0DE0FB4822","Limit internal frame rate during startup (speed hack)":"F6BF450D22104FD2527122EA6E81B1F70E916B95"},"GHQP7D":{"16:9 Widescreen":"AAE14CDC13A0C082A850DED85417100A8C8A0687","60Hz":"884A33613AE8D916128E3FF39B3AA9F63DAADC58","Limit internal frame rate during startup (speed hack)":"BD475ADEF145AC9226514CD1B2D4D5F3BFE67964"},"GHSE69":{"16:9 Widescreen":"3538CDBD13FA939B7BA5F0F82253401AFDD1E2CD"},"GHSP69":{"16:9 Widescreen":"409754E2EBB6F05DEE1AFC647E25B15D462638FF"},"GICD78":{"EFB Copy Fix":"3A94591A149AE88C150AB3320BBC909FE54BAEA5"},"GICE78":{"EFB Copy Fix":"5BF55685B8867A85EAA9C86571309B17BF7DED32"},"GICF78":{"EFB Copy Fix":"85AABAEB9A59C4F96D9330A3B884F6D757DA1683"},"GICH78":{"EFB Copy Fix":"3A94591A149AE88C150AB3320BBC909FE54BAEA5"},"GICJG9":{"EFB Copy Fix":"969134EA21A160EBDA91C0870266E7D1707FDC43"},"GICP78":{"EFB Copy Fix":"13B158CF41F5412BC637F50644193D43CC3DA49A"},"GIQE78":{"EFB Copy Fix":"E15AA1E30D26E5735D68AAADE436E7B7E4A33A35"},"GIQJ8P":{"EFB Copy Fix":"FFFCB76E98DDB06A7BBBC0AA73C869C87EB787D6"},"GIQX78":{"EFB Copy Fix":"485DA99FAB35646DAA2A138B0315361495ABE778"},"GIQY78":{"EFB Copy Fix":"485DA99FAB35646DAA2A138B0315361495ABE778"},"GK2":{"16:9 Widescreen [Vague Rant]":"6B4C59C2A2C5D71C7A38C8513A76E6467E307A26"},"GK2E52":{"60 FPS":"8F2397930A50C3C176188526EB9612578F9CBCE5"},"GK4":{"16:9 Widescreen":"4652969A4DA869FD28F2CAA3869A38F5C2AAFEB1"},"GKB":{"16:9 Widescreen Region-Free":"4652969A4DA869FD28F2CAA3869A38F5C2AAFEB1"},"GKBPAF":{"60Hz":"8E4E0ABA0E6D102A33206F34F4DEE63159B3CB4F"},"GKDP01":{"16:9 Widescreen":"DF4657937DE730107B636C73E959277EB963D210","60Hz":"6740D7B914CC897AF50798C0AB391965294FD1E1"},"GKRPB2":{"16:9 Widescreen":"69C01A4F91D991CF866237FED94A138308FC104F","60Hz":"40D06A5C7A3D873ABC73DEDF573738C2E3E37FF0"},"GKWJ18":{"16:9 Widescreen":"696570101FA040778EAD310377C484C846D87430"},"GKYE01":{"16:9 Widescreen":"6520EF1B7D88F00747B120A3A010458602ED989D"},"GKYJ01":{"16:9 Widescreen":"47B8EF7D02831AA5C375C698797BF2D5475FEFF2"},"GKYP01":{"16:9 Widescreen":"69413C75036D2975716066E6574461B981FF0124"},"GLEE08":{"Fix audio issues":"7355F358CAC6F418D37E4C23E64F7867D46E4FC9"},"GLEJ08":{"Fix audio issues":"12B24A6D7389A2AC5AB75FC0BF8493E7661F2A73"},"GLEP08":{"Fix audio issues":"81BD39F5527552DE89E3B59BA86298900F0A3168"},"GLSD64":{"Fix freeze in opening cutscene":"5E2A73717BD66EF647846DD64C33BC80AD9B5227"},"GLSE64":{"Fix freeze in opening cutscene":"1CE78E7954415A44DF693C0BB879AA5A4FF059A3"},"GLSF64":{"Fix freeze in opening cutscene":"009B0C4AD80A9C28C987934D254C2C4AACC9A07A"},"GLSP64":{"16:9 Widescreen":"8E7A544C10E7A5E2F0304A0D2586879627EF6586","60Hz":"B67144E87B54246137142992A3BC83DC13BE68A7","Fix freeze in opening cutscene":"3D0894616C9A7FA5ED91C1D2F461BF14DF47ECEC"},"GM4E01":{"16:9 Menu Backgrounds Add-On":"87D1A6A3E29ADA03B0D29C2F1841C18E62DB0A15","16:9 Widescreen v2":"78BE50F93E81A1972CA31ABC318949E366E82BD0"},"GM4P01":{"16:9 Menu Backgrounds Add-On":"87D1A6A3E29ADA03B0D29C2F1841C18E62DB0A15","16:9 Widescreen v2":"6D950B6EEFD6D304E1A424355B74A595D32BDF2F"},"GMBE8P":{"16:9 Widescreen":"D12DE9E3941BCE6EB50B8DA92140E09AFC0104C8","NA Stretched HUD":"0151A6683E1FBFD20096807C54A6952C4A4BAD40"},"GMBP8P":{"16:9 Widescreen":"8A688981F33A2C53882EF08FCF20B88AD43EF417"},"GMNE78":{"Limit internal FPS to VPS (improves performance)":"DE38BC8493EA24F93B6D805F28746AA99C53C677"},"GMNP78":{"Limit internal FPS to VPS (improves performance)":"65FB9B430F7D58EBC76DAADE8CA9DBBAA32B8B51"},"GMOP70":{"16:9 Widescreen":"CE4D298616BD42E4D8F8936B325CD1FD7F5B956B","60Hz":"0CD2CC787A5FF551901E41C85C8AAC02017ECB89"},"GMPE01":{"16:9 Widescreen":"4F98592DB3DEE3857469A8D8605FAF4BD6F7C76D","QOL - Allow Digital Presses for Map Screen":"E232B27564E9AA0C32DE163C9C056317A7B2B12E","QOL - Automatically Advance Text Boxes":"59607671BFC4717ACAF9807BB7EB0D9F982866D4","QOL - Disable Advance on Results":"CAEA37F3FEF89400513353EB85875F2A3AF4C03E","QOL - Faster Boot Time":"F75FBBD838C5B84FF687962FBA9195B217DE132C","QOL - Increased Board Speed":"19F264DE7F07EAC9433CA4B591BEBA1EC976C9F1","QOL - Increased Taunt Capabilities":"C70391D4961A0E820DE40141C89341369A9F021B","QOL - Instant Text Display":"F4E6913CF034E5778B9F9CA5FF448FF1B78B3333","QOL - Rumble Always Off":"93621947019532F02C25937FD3EEEE21A788CB07","QOL - Show Controller Port Number of Who Paused":"D7249AC3C949FEB860CF4350C5B7B79AC16569F1"},"GMPJ01":{"16:9 Widescreen":"A795811F2F0C92D9DCABDE97D9E39B47B1DBCEFD"},"GMPP01":{"16:9 Widescreen":"A795811F2F0C92D9DCABDE97D9E39B47B1DBCEFD"},"GMSE01":{"60FPS":"2805B1A0DD53EB64877D375D10F667700955720F","Widescreen":"BD718F961DBA5372B1D0257D454D535746C453A0"},"GMSJ01":{"16:9 Widescreen":"E8112A01040A06460E368F142C5D1FD0B4085D94"},"GMSJ01r0":{"60FPS":"9894B5B6B5215844D48411021FF8EECFE851D79D"},"GMSJ01r1":{"60FPS":"2DB69DB7A3753D543CD85CA4C77B4EF0AED7486E"},"GMSP01":{"60FPS":"D07009B710B3BBC6B12E54ED6A7969D58197EDAF","Widescreen":"0B7B89BF0868393076EF5F9DBD689DD0EDBCB84C"},"GNHE5d":{"Nop Hack":"89393A24E2336841AA4CD0AD3BE1C9A66B89E9EF"},"GOCE5D":{"16:9 Widescreen":"E4D800B90F16A15D9252EED1B2A23AEAF5CD230A"},"GOME01":{"16:9 Widescreen":"C33CBBF6F22195BF626104B4827200B3EE1CF30C"},"GOMP01":{"16:9 Widescreen":"874784CCD4AC3DC9C69654E471C18AD0F5E4D9AE"},"GONE69":{"Widescreen Hack Culling":"6412A67E1C26E146A9526AFC9F0EE517B214069D"},"GP4J18":{"16:9 Widescreen":"BF4D2D6AF66F285E81173B3B20D5369DB6FB351B"},"GP5E01":{"16:9 Widescreen":"5ED053787332DB5EEB8B963BA5DF197B58C6BB75","QOL - Automatically Advance Text Boxes":"41EF84663008668682BB75DC8B874E38669AE041","QOL - Disable Advance on Results":"6C36BEFE751131298BC8B39012892F6A6903CE60","QOL - Faster Boot Time":"8E952E6E12639AC20F6F9FDD150C12F0C294DF43","QOL - Increased Board Speed":"4BD72B8ED75ED8EFB3329C104BAD114FDDB8AD2C","QOL - Increased Capsule Throwing Speed":"C5BA3C9D386D09DD6CB9242A12A0D041BEB93160","QOL - Increased Taunt Capabilities":"C360EBDDCB0237076052C983BD9719411CA67CDE","QOL - Instant Text Display":"2CD4A06A3B9C18CC9EA2A2DEBD96E066CD1D700A","QOL - Show Controller Port Number of Who Paused":"5BA3648D18346EDC97B23475D4748C637E6095A2"},"GP5J01":{"16:9 Widescreen":"81E69B6BEE85E06805EC24E1CFEA9405BF459DF8"},"GP5P01":{"16:9 Widescreen":"FD4FCCB97C06F0173A30D4CC9A99422B2C0ABA9B"},"GP6E01":{"16:9 Widescreen":"B502AF0887792E9B140516D14BA75F99772C17BE","QOL - Auto Advance Text Boxes":"E645DF70CE0266D174D917A817A7FBEEE974A9F0","QOL - Disable Advance on Results":"798E566B621F6A32BF23C588E76EAC0776E2A8A3","QOL - Faster Boot Time":"B089A3C80D5DA86ABF6258F8BFB1DC78BB54A973","QOL - Increased Board Speed":"501C52ACF117950888A66DB2BEDBDFD7D9B20235","QOL - Increased Orb Throwing Speed":"845F439278DA8CBC225658FF4E2F82D707422BFB","QOL - Increased Taunt Capabilities":"0AED27341A06186AD92D5725C0B9693CA8F85B7B","QOL - Instant Text Display":"5AAC4E21927D4AEE7AC329CE793D4CDAEDDF7A3F","QOL - Show Controller Port Number of Who Paused":"DF5D2F068D76ABF2B1C294D413B9154B8C356929"},"GP6J01":{"16:9 Widescreen":"88F50F8298F82E3E5C161277BD4A985CE893A9D0"},"GP6P01":{"16:9 Widescreen":"DCCE59CA987624C753F0A86BEBA4287635901C93","Remove Black Bars":"138A86D43B5830BFE1926F58C0A4FBE2971BB02D"},"GP7E01":{"16:9 Widescreen":"87EBF72D90503EBC5CE9BC994795669E48A92A4A","QOL - Auto Advance Text Boxes":"82338AB5D82A7F398B4EB674CA4866DD1F4A1E65","QOL - Controller Options Always Acesssible":"D2F9CC0B9E52136E77187395FB7DA9B64B2C3B30","QOL - Disable Advance on Results":"A644551162A61DE7997B2A84F5BFDEFFD3FC2FB5","QOL - Faster Boot Time":"1939B5C575AE8FF6890BDDDD1B3FEA472D8CABA0","QOL - Increased Board Speed":"7E06A2EBCFECADB8074152B489984237A6FE9F21","QOL - Increased Orb Throwing Speed":"6671BEE71920D6B845AE1257AFE3AB3B95CD6FBD","QOL - Increased Taunt Capabilities":"9F60923F95FFE709CCD06966CD2DF743263934D1","QOL - Instant Text Display":"48FA6002E42DA9BB83ABC980A32C60CE3B21C4FC","QOL - Show Controller Port Number of Who Paused":"5E6F8C6C87D5944DB18A4E4F9A45F61EE87A4163"},"GP7J01":{"16:9 Widescreen":"A081F0729068D233E99DC00C64E36CA33C945640"},"GP7P01":{"16:9 Widescreen":"75D6CDA4EA301A71F7701A0487544E79136850B9","60Hz":"6D0D5B7CA58C38A3147F8ED981B5B21CC6C519CD"},"GPIE01":{"No Blur":"A25CDDB79991F090BD7F12840AD06F13BD5CCF95"},"GPIE01r0":{"16:9 Widescreen":"2B58166A66644F984E64077A6245C94C79B51063"},"GPIE01r1":{"16:9 Widescreen":"9A1545AB4E2B9216B95AFFA83420D723512DA8C8","60FPS":"3337C767EAA21D30C6CFEAE9985FFCABFC63E2E3","Disable Pikmin optimizations":"CBD15E9104929F0631713AE482475CB2B90E71F2"},"GPIP01":{"16:9 Widescreen":"5AE44D5B0E0D4C412B453B95CF5A41DBA4D685E6","60Hz":"AE59FF751E167632AE283F38CC4B96BDF7B9B81F","Turn off blur":"4ED79A548DBD7DBD35928A2F1138315FF103E260"},"GPNE08":{"16:9 Widescreen [Ralf, darkludx]":"ED3D9BB08C5F1D21BC8D5C73155C00E3C25C466F"},"GPNP08":{"16:9 Widescreen [Ralf]":"469B18FE8219031D355557B836EF02B625040A4F"},"GPOE8P":{"Make Save Copyable NTSC Port - 1.0\/1.1":"7A4B3E596BA4FDA8F3FCF984079B3F44CB2D6AB3","Make Save Copyable NTSC Port - PLUS\/1.2":"304DB311F2107E88EBCE058535B266F9263AA8E2","Save Validation Code v 1.0\/v1.1":"9F6259132453DCC0607EAB35DB182C76818F2F9F","Save Validation Code v1.2\/Plus":"17EB6CFB408EF27D44C053A1336C3B87B6A05018"},"GPOE8Pr0":{"16:9 Widescreen":"99986BB1D4ABE45C429D48B01BFB578FEA69C14F"},"GPOE8Pr1":{"16:9 Widescreen (Plus) [Ralf]":"5563671A6702785FFE106DAB7EF3F4FE6D9F705B"},"GPOP8P":{"16:9 Widescreen":"DF133C61C29D5CFA87A2A785C962C205F80E6D04","Game Save Valid":"40EC1590DB26C7C58D00E3065F1E404341EF0E73","Game Save copyable":"2F5FB98927DAFF141D1CCD1EACA8AF626397E284"},"GPVE01":{"16:9 Widescreen":"E5B51080CFD518FE584CF3B61A1099CD032479D6","60 FPS":"ADF5E3BBF0002A76949D0C11C51E58089BACFEED"},"GPVP01":{"16:9 Widescreen":"78F3C9A789827B063A6701987262276A66ABD82B"},"GQPE78":{"EFB Copy Fix":"880B114E9A308084CAB92C004A9EE067B371C310"},"GQPP78":{"EFB Copy Fix":"5D9A14954AE8D639C9B254F3BA73A70F284BBC8D"},"GQSDAF":{"16:9 Widescreen":"CA82C44B2E8FC5C184E3FF935BC89661B7DB55D6","60Hz":"952165FD78543EB6D2F5230F7570B0513773D332","No Blur":"DEB7DF358423F0EF30C9FD49F4F14590836D52A6"},"GQSEAF":{"16:9 Widescreen":"48AFE1D32843C6B5AD4337A49F15FD447DF2A752","Remove Blur":"EF448CF1FA6A15EB2661EA5338703C13D52EF65B"},"GQSFAF":{"16:9 Widescreen":"76C30E1737CC5C3B5DE32532B5F9A5CD50B690F8","No Blur":"986BAE5103CDE9286D2D34FD28FE2540D0759AAB"},"GR2E52":{"16:9 Widescreen":"5F55CE7DA7E1E3261CF2FF88F34CDA63BA511003"},"GR2J52":{"16:9 Widescreen":"D3C6FA690BED07BFA65C2E1FCDE2FFA5AA0EEEE4"},"GR2P52":{"16:9 Widescreen":"D7BCADD4E76B5E3F7D9D463F55A2777528E37E94"},"GRNE52":{"16:9 Widescreen":"483D74C47AD1012606D591A226AB3144C7FB201C"},"GRNJ52":{"16:9 Widescreen":"949281B7221B20680B7BC29E4754B73A326EBE85"},"GRNP52":{"16:9 Widescreen":"11051B094FE5A8B2E73060BFF786C1588E36979A"},"GROE5Z":{"16:9 Widescreen":"FADBAB5EC280CF8ED4C9536A33F4184BA210C9B0"},"GRYE41":{"Disable Culling to Fix Rise and Shrine Hang":"AF0A575EB6071EAC0D2EC3D2EA30A23EB05A4192"},"GSAE01r0":{"viWidth 704 Aspect Ratio Fix":"0D529180B9B28BB06E21EDBDBA61C17EEF0FFB7F"},"GSAE01r1":{"viWidth 704 Aspect Ratio Fix":"CB15ED22625690A5DBE4607FE30160125421461D"},"GSAP01":{"viWidth 704 Aspect Ratio Fix":"749EDA62B1B73354599929F1B50E992E111C5493"},"GSNE8P":{"16:9 Widescreen":"CFDF3FA5B48B347E49E8E521D7BE47FCC9A5CBF4","60Hz Aspect Ratio Fix":"D90F66C0D46D9BEF27C8E13BC8804DB1F382C8B1"},"GSNJ8P":{"60Hz Aspect Ratio Fix":"98DF9D67DE77F66A4B09F25C5621805DEB40865B"},"GSNP8P":{"60Hz Aspect Ratio Fix":"D90F66C0D46D9BEF27C8E13BC8804DB1F382C8B1"},"GSO":{"16:9 Widescreen Region Free":"0D60D0593F6DA28B1236381B22082506B9533F0E"},"GSPE69":{"16:9 Widescreen":"80E18A8963F4EFD3C03494C95934452FCB6E521B"},"GSPP69":{"16:9 Widescreen":"162E87E9F5511CD82216811055836C11B374C45B","60Hz":"E5DF2DF34D2F0BDEE4205C891576511EC572C5DC"},"GSWE64":{"16:9 Widescreen":"6480326CCA8FF64A4868EC6E9FDE9F38819A75A1","Disable Dithering":"85F65E4B627641862C1A6091D29A3BB0626B04C3"},"GTEE01":{"16:9 Widescreen":"6B57EBCB9CC0ACE2EDA0F13E3F1F55F3FABC3A26"},"GTEP01":{"16:9 Widescreen":"72785784363C46180AF8E388348FD77667E7D382"},"GTOJAF":{"Remove Blur":"53A2EDF113F2ED7E54A16AA0E73FDB2C44C79DE5"},"GTRE78r0":{"Limit internal FPS to VPS (improves performance)":"42B85EFC5DEF8B243521EAD434DDCBDF95664CB1"},"GTRE78r1":{"Limit internal FPS to VPS (improves performance)":"ADB3A41B992EAB15AE484B2E575ACF3354415186"},"GTRJ8N":{"Limit internal FPS to VPS (improves performance)":"47DBEFDCF22ADC2AB148A54AC885FE19B3C07E3F"},"GTRP78":{"Limit internal FPS to VPS (improves performance)":"E8752E1B60DF76CD0CC7408074E742B0966BC904"},"GTZE41":{"16:9 Widescreen":"ECCE87E0006475C73E7D936AA0150BD20166F9EF"},"GTZP41":{"16:9 Widescreen":"62D8F159B93167171E0860D96C4BB32A5BF5648B"},"GU2D78":{"EFB Copy Fix":"CFF4C3F932B08732627572EDA1A0CD2D9C71AE0C"},"GU2F78":{"EFB Copy Fix":"CFF4C3F932B08732627572EDA1A0CD2D9C71AE0C"},"GU3D78":{"EFB Copy Fix":"8A0E3114862ADFE421874211BD6F5220AA425BF5"},"GU3X78":{"EFB Copy Fix":"E3303FDAE7ECA17A72EDC440C32D94648A6453A0"},"GU4Y78":{"EFB Copy Fix":"D54767785E139A8BC8C4B75573FBD5A0B686D8E3"},"GUNE5Dr0":{"16:9 Widescreen":"C942EBCBE8A487C25E296EC1FAC2358DA1487DBD","60 FPS":"A441630EC5FF0EB74D2243A3092D22E69C6BEA02"},"GUNE5Dr1":{"16:9 Widescreen":"39BD84CBE2AFACFEDEC5E9020D1A8B0D36DD53F9","60 FPS":"05779F1A3D0C43305D52A95447D612CB424942C0"},"GUNP5D":{"16:9 Widescreen":"FE3BA1DAA1AF278A7839E27BF466B0BA0B390EC2","50 FPS":"3B20499A3F8D1D7CCA5B0015F2D80ECED25842A6"},"GV4E69":{"Fix 2D Rendering":"8679891FCAA250FCFF670B26E0CB9875900D17FD"},"GVJE08":{"16:9 Widescreen":"09EFDA75D876A675121C8344D8FDD09A70A1A846"},"GVJP08":{"16:9 Widescreen":"93938F4C6112C91549F2B52E6259170F7501EC07"},"GVPE69":{"Fix 2D Rendering":"3159CA79B0A890131763EA6CB163684BEE886E3F"},"GVSE8P":{"16:9 Widescreen":"4CE9C491160A4B631142EE9CE802C694163F1CA3"},"GWRE01":{"16:9 Widescreen":"AC42770B06662BE1DC863EC80F44B5E034C63664"},"GWRP01":{"16:9 Widescreen":"9DDDFAB28C4BD35CF64050E1EF684DC042B1AFFA"},"GWWE01":{"16:9 Widescreen":"98B2E75D8E1CED4A964D3129A5DC10E30538CAA6"},"GWWJ01":{"16:9 Widescreen":"2DAD9A5E2A140F02CCBA727C4BE7C74BAC156778"},"GWWP01":{"16:9 Widescreen":"2DAD9A5E2A140F02CCBA727C4BE7C74BAC156778"},"GWZE01":{"16:9 Widescreen":"9EFC191DE6D21A1681FE241AB2EE4A131259F317"},"GXCE01":{"16:9 Widescreen":"901A1E78A3A0124F55548507D3B3707125C64A8A"},"GXSE8P":{"16:9 Widescreen":"F50BBA440184FC77A4DFFAA58FF2BB888E2E072F","Aspect Ratio Fix":"4214C74DFB8A74FC3AA4A643E869BC4D9A38EDA0"},"GXSJ8P":{"Aspect Ratio Fix":"227909607984BBC3D36AAC7DB9DFE385F3363C49"},"GXXE01":{"16:9 Widescreen":"7CAFDAD7B5E7459CCC1BB209D439DDCBB5BE4E4E","60 FPS":"7E2829049003FC9DC8BC28044B33CAFF3768B54C","Allow Memory Card saving with Savestates":"64FAA15062F0D0C319F904BBDE9C4489A25D6369"},"GXXJ01":{"16:9 Widescreen":"AF704A8C6838FDF47054D9EDD9106B3F46781FA9","60 FPS":"4F5FD45B4EC1B33FE076FA06F8FF863C46A06BBA","Allow Memory Card saving with Savestates":"8293802260536FA2EF2EFDAB5266DE36BB88DE1B"},"GXXP01":{"16:9 Widescreen":"02716B2585BEE74F8FCDD97A78F6A0D3DC7F331B","60 FPS":"BF6171EA5CCF4B57AA596372589DDB5E6904DD7A","Allow Memory Card saving with Savestates":"3CAFBC4AE6FC5CE9F53377F86AB5BD8F1BC8861A"},"GYFPA4":{"60Hz":"402ED10AC842041AB4B39AE8F2D81B2D7AEF9CB4"},"GZ2E01":{"16:9 Widescreen v2":"E7521ED27BFC972628906CBE8D5403ED57253BB8","Hyrule Field Speed Hack":"FCB673D46E716C7F63C618B8D8BF83AEE0B501F0"},"GZ2J01":{"16:9 Widescreen":"F985A0A58D8E2B23E8A557FAFF8D367AFFEADD07","Hyrule Field Speed Hack":"FCB673D46E716C7F63C618B8D8BF83AEE0B501F0"},"GZ2P01":{"16:9 Widescreen v2":"CAC38B0D334B925A57AA3118D35932B8A185137E","Hyrule Field Speed Hack":"0F63623D4D984B7706F718F57C0ABDB6DBADCF8D"},"GZ3E70":{"16:9 Widescreen":"167F9E9A9B372CB3A01F308B46FF1403F8599C51","Deinterlacing Fix":"EF1291C0E45277CDAE4D0F61E1949C1E75E5A3E1"},"GZBJB2":{"Deinterlacing Fix":"19D67BD12A18F0E3A960BBB92E2F490BEF5C00C1"},"GZDP70":{"16:9 Widescreen":"2E424E0BBAE6EF5D6A8FB4224EA1D0746BAC37D3"},"GZLE01":{"16:9 Widescreen":"9FA864EE7DD8CE7FF538EB4E0243F20137430BD8","Remove Distance Blur":"78EA34CEF8E01701491C280F155F1C12EC9BF1A2"},"GZLJ01":{"16:9 Widescreen":"E03B61989025CA33937E63A057E6E40A403811BC"},"GZLP01":{"16:9 Widescreen":"3EFFF6C52B5633A1729FAA6883D8579E77F7D057","Remove Distance Blur":"14BECBEA4DD281EBD0F7FE7DEE8020B6F2418ACD"},"GZPE70":{"16:9 Widescreen":"7142F2495507AC7136992128ED0FCA6BC72B61F4"},"GZPP70":{"16:9 Widescreen":"591FD6C9668C79FF1CE8558EBED2486A7A327F05","60Hz":"A59B84DB5486521AEE1C23B6C741ECD35216E5AD"},"GZSE70":{"16:9 Widescreen":"8E86EAC7EA4F4D2854DD9020CD795630CA64C4EE"},"HAF":{"BufferPatch":"181195871F63B89B1CF09AFA4420CF89B9883108"},"HAL":{"RSAPatch":"AD12237401ABE9FE4A545AADB5C5AE10355E2076"},"R7XE69":{"Speed hack":"53F3273525BB719D535007FB97E7F381D808A8A9"},"R7XJ13":{"Speed hack":"4590D425BCBAFA0D1E32D27B46068843F6A2BE09"},"R7XP69":{"Speed hack":"53F3273525BB719D535007FB97E7F381D808A8A9"},"RB7E54":{"Limit internal frame rate in loading screens (fix hangs)":"BD1A252B43FD339391ED33554542E786BAD6671B"},"RB7P54":{"Limit internal frame rate in loading screens (fix hangs)":"90247F89DF8EB7A4AFA2C425CD3006257A95A8F8"},"RGQE70":{"crashfix":"5F4CF8D4DA19A0FF74FF9EB925AC0236069BFD59"},"RLEEFS":{"Fix crash on main menu":"793642AC6862C2F3412035A9E3D7172CC4A1D5C7"},"RM8E01":{"16:9 Widescreen":"E61344EB1542A78D497981C307B6549985C7A05A","Extra - Disable Music":"25223F9EFAABF601CAC7810004F124E4056598B1","QOL - Faster Boot Time":"BCC4279F8B28636AD773F01540E78DF40EAD6087","QOL - Increased Board Speed":"B65AF1819966CD3435D88801E8C79704E3A52DB5","QOL - Increased Taunt Capabilities":"66495D7CB532FAE778AFC22CF45D17D0FFDE5310","QOL - Increased Text Display":"B4186DDC54F33F4D6A22188EF50CEB43FB205673","QOL - Invert IR Stick for GameCube Mod":"9D90A9C66AE8AD91B201B40C4145D1323B701A77","QOL - Remove Explanations":"330DD53AB993A99576564FEFD222D7BD211B878F"},"RM8J01":{"16:9 Widescreen":"A140BDCB8E1721CB6B4CD878E412113322258B57"},"RM8P01":{"16:9 Widescreen":"F664C32AFD3D785FC6E04D8990A3FA1C72A18C5C"},"RMHE08":{"Bloom OFF":"CCF233DA57B3E75221870DE502955114B0D4E7FA"},"RMHJ08":{"Bloom OFF":"29D3625B7ED577587E56AA07CB0EB8C47C97E823"},"RMHP08":{"Bloom OFF":"1720C1173D4698167080DBFC4232F21757C4DA08"},"RO2P7N":{"Hangfix":"EEE9C8DE4671C18DD7F81DD08D39B64C57600DEA"},"RPBE01":{"Fix black screen effects":"775ABECA6073E02C5C68CF4D644194D966A418F5"},"RPBJ01r0":{"Fix black screen effects":"0EAB5D8DE827894AFEF97C10ACB67378E6983323"},"RPBJ01r1":{"Fix black screen effects":"4905E08643E9D00136F7EAF51978CF2F54D10D07"},"RPBJ01r2":{"Fix black screen effects":"4905E08643E9D00136F7EAF51978CF2F54D10D07"},"RPBP01":{"Fix black screen effects":"82AEB60F9A9083F93060531A970FFAABE0833A40"},"RRBE41r0":{"Idle loop speed hack":"20C7A2D4A3D6D55A8029A9D200D57CBD11F60B88"},"RRBE41r1":{"Idle loop speed hack":"0A30FA4C7E787197A00B7267368F372EFF623776"},"RRBE41r2":{"Idle loop speed hack":"2227787007F16F0CD454422AE6EB94656464B6D0"},"RRBJ41":{"Idle loop speed hack":"E9209EB728AA13375B2ABEF1C15D066DAFBF2EBF"},"RRBP41":{"Idle loop speed hack":"8A47BC311138EE3A11E049DF3EE6AED38990FB49"},"RTH":{"Disable blur":"812EE46AC967BFCD239335B10A664D71A93E8175"},"RX4E4Z":{"Fix file reads (dcache bypass)":"9E4E0F1465A9A1E85349DBA3B1278AC215A97DBB"},"RX4PMT":{"Fix file reads (dcache bypass)":"EE85907C03F0295794821383B93F8D5B91D2697A"},"RZDE01r0":{"Hyrule Field Speed Hack":"15EAD073414C9903D6CAE5229DCE582BD17A9162"},"RZDE01r2":{"Hyrule Field Speed Hack":"27395CC8BC2C51201D566657D31A471A850482FB"},"RZDJ01":{"Hyrule Field Speed Hack":"B3F7473F8C911A32F1D616491C9E78EBBD7A6309"},"RZDK01":{"Hyrule Field Speed Hack":"A280C0114B800D7DC056ECFB5E482229DA0B1550"},"RZDP01":{"Hyrule Field Speed Hack":"2A83ADFB760F9498841ED0ED68B0C0438232472C"},"SAOE78":{"Fix crash on boot":"EA11FA4908FB20B61876ACD360EC7657A6D39FB2"},"SAOEVZ":{"Fix crash on boot":"AA55C214DE7545DE0E203CC39F06BF3D31451BE9"},"SC2":{"Limit internal frame rate (speed hack)":"B3FB0FCE108E85E6198BAF3535850470049E2BE2"},"SGLEA4":{"Fix black screen":"258378187ACF475A55EFEAF8A703681252E014C3"},"SGLPA4":{"Fix black screen":"6F8CD59D897338CA90939149E1A62588620C6D88"}} \ No newline at end of file
+{"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":"478DA1D596D8513C343850719155813CB61A56DD"},"GCBP7D":{"16:9 Widescreen":"478DA1D596D8513C343850719155813CB61A56DD","60Hz":"DF2C4DDF082C9E67919443C5ACC485AB2CF7F8D0"},"GCCE01":{"16:9 Widescreen":"BA0066518E7EA8599A81F8496BD39DE20CC369D6","Fix GBA connections":"483BDB94615C690045C3759795AF13CE76552286","Fix buffer overrun bug (crash at Goblin Wall)":"6C107FEC15C76201233CA2645EB5FAB4FF9751CE"},"GCCJGC":{"Fix GBA connections":"4C104D24329172F5D0F8649DE9423B931FE72CA3"},"GCCP01":{"16:9 Widescreen":"2580E7379633CF4DE13B9EC945DB17543477828A","Fix GBA connections":"2EAA60A8A115AD68A795109FB59E4A726D29016D"},"GCN":{"Fix C4 texture tiling (used for buttons and some character icons)":"601FE183C9524ACCF068874DABD73921C86769CF"},"GCVEEB":{"16:9 Widescreen":"ADA686C90189D1D7D23E23E525EFC96EFA104BB9"},"GD7E70":{"Deinterlacing Fix":"6F8B6E33214162E5CDEBD2BC6A60FAFA04D2EB10"},"GD7PB2":{"Deinterlacing Fix":"3B798C9D63E755349B253FAF9186BAE1F703D96A"},"GDREAF":{"Fix audio issues":"F8EAE60FEB0CFB4477FDC4B9E136B63F68DFA63A"},"GDRP69":{"Fix audio issues":"E23D98B2CE185C3993A40F2495D37E41B971BF91"},"GEDE01":{"Fix startup hang":"21068C3CE905FB0CFFAA7408A93154AF8A5295A2"},"GEDJ01":{"Fix startup hang":"7061F3CF11BF64D3BA7F32CCF2BAC42FF3614AB6"},"GEDP01":{"Fix startup hang":"6F1B00517CBA30BEB738EAA90E71221378CD570D"},"GEME7F":{"Force Progressive Scan":"CB04E00918C9C0F161715D21D046ED6620F7ADEF"},"GEMJ28":{"Force Progressive Scan":"41D8F2C833C0B7B79FCA038752052059207AE4D5"},"GEZE8P":{"16:9 Widescreen [gamemasterplc]":"5CEBCFBEA4E444495D2FD6D8B6607DFB2349CC1B"},"GF7E01":{"16:9 Widescreen [gamemasterplc]":"C59CCA3EF8A5E97B32EB64DB9AE80E652ED281C6"},"GF7P01":{"16:9 Widescreen [gamemasterplc]":"1230053B5C347B62E432EFD6635433A183D18619"},"GFQEA4":{"16:9 Widescreen":"5CD9568CE58EF82EB371594667015C9B799454B9"},"GFZJ01":{"Make Save Copyable":"F659D22CB1DFF15C3915D7630D838EED8DB0BA48"},"GFZP01":{"Make Save Copyable":"F659D22CB1DFF15C3915D7630D838EED8DB0BA48"},"GGTE01":{"16:9 Widescreen [darkludx]":"EF128E7A9C22676834F558BA0F0F7FD8B9028727","Experimental 60fps":"D40344111D989EA009901F8B1C45B5AC8D39E6D2","Simple 60FPS":"5232A937D1D813FF58DD71D716284ED6AB535689"},"GGTJ01":{"Experimental 60fps":"BEC2310911003AF9E4B984A4051E0F885B8CC825","Simple 60FPS":"D62E777A2F019D7BD0AFFBCE876BD9AE408F1667"},"GGTP01":{"16:9 Widescreen":"FA9CD330ECDA01275DA88BD0803DE47757D16A4E","Experimental 60fps":"EB1ACD613BB432A3391CFFF1F0145A6ACE66B210","Simple 60FPS":"43FAD0DDD130BE1E4C8C7603EF6CA7DAA0DF5EE9"},"GGVD78":{"16:9 Widescreen":"9D07DBB5EC2FAA47F2E4587FBD75FD6F3E7E91B0","EFB Copy Fix":"FE52240DF6D132C15A8324E8A477F2BF2250D208"},"GGVE78":{"16:9 Widescreen":"86E561452235BF88D41884558EF34F54CE0FEB48","EFB Copy Fix":"5E38E10829D5F77243C95E9E41518BB3ADE24139"},"GGVP78":{"16:9 Widescreen":"F9B2108D833084FA36A53E00F1647A9579F847D3","EFB Copy Fix":"5E38E10829D5F77243C95E9E41518BB3ADE24139"},"GGVX78":{"EFB Copy Fix":"740F2D1C01DA39D1760D96B03974A48E6F74578D"},"GHAE08":{"Fix audio issues":"9799AFF8463EC86C9230E31E2627E141F0C129D3"},"GHAJ08":{"Fix audio issues":"B45A8FC32D14567B8D6C95F303E00A72C0E1D344"},"GHAP08":{"Fix audio issues":"BC7F3CFC97593AA2055C370C175950DC478D2709"},"GHKD7D":{"Limit internal frame rate (speed hack, also fixes audio and gameplay bugs)":"1AF09E98F0B72833A0F4E519EC3D4130C36D615E"},"GHKE7D":{"Limit internal frame rate (speed hack, also fixes audio and gameplay bugs)":"9BFBDE9184BC05EBD545625DEE486E3AAB36B6A3"},"GHKF7D":{"Limit internal frame rate (speed hack, also fixes audio and gameplay bugs)":"919E853513192C97F3CF49246A65ECBF39E7C201"},"GHKP7D":{"Limit internal frame rate (speed hack, also fixes audio and gameplay bugs)":"1AF09E98F0B72833A0F4E519EC3D4130C36D615E"},"GHKS7D":{"Limit internal frame rate (speed hack, also fixes audio and gameplay bugs)":"919E853513192C97F3CF49246A65ECBF39E7C201"},"GHLE69":{"16:9 Widescreen":"742BFC79D8F0BA5D6215772B58F5A0EADD7BFAFF"},"GHQE7D":{"16:9 Widescreen":"520E5F718D3BD1291C55021776091E0DE0FB4822","Limit internal frame rate during startup (speed hack)":"F6BF450D22104FD2527122EA6E81B1F70E916B95"},"GHQP7D":{"16:9 Widescreen":"AAE14CDC13A0C082A850DED85417100A8C8A0687","60Hz":"884A33613AE8D916128E3FF39B3AA9F63DAADC58","Limit internal frame rate during startup (speed hack)":"BD475ADEF145AC9226514CD1B2D4D5F3BFE67964"},"GHSE69":{"16:9 Widescreen":"3538CDBD13FA939B7BA5F0F82253401AFDD1E2CD"},"GHSP69":{"16:9 Widescreen":"409754E2EBB6F05DEE1AFC647E25B15D462638FF"},"GICD78":{"EFB Copy Fix":"3A94591A149AE88C150AB3320BBC909FE54BAEA5"},"GICE78":{"EFB Copy Fix":"5BF55685B8867A85EAA9C86571309B17BF7DED32"},"GICF78":{"EFB Copy Fix":"85AABAEB9A59C4F96D9330A3B884F6D757DA1683"},"GICH78":{"EFB Copy Fix":"3A94591A149AE88C150AB3320BBC909FE54BAEA5"},"GICJG9":{"EFB Copy Fix":"969134EA21A160EBDA91C0870266E7D1707FDC43"},"GICP78":{"EFB Copy Fix":"13B158CF41F5412BC637F50644193D43CC3DA49A"},"GIQE78":{"EFB Copy Fix":"E15AA1E30D26E5735D68AAADE436E7B7E4A33A35"},"GIQJ8P":{"EFB Copy Fix":"FFFCB76E98DDB06A7BBBC0AA73C869C87EB787D6"},"GIQX78":{"EFB Copy Fix":"485DA99FAB35646DAA2A138B0315361495ABE778"},"GIQY78":{"EFB Copy Fix":"485DA99FAB35646DAA2A138B0315361495ABE778"},"GK2":{"16:9 Widescreen [Vague Rant]":"6B4C59C2A2C5D71C7A38C8513A76E6467E307A26"},"GK2E52":{"60 FPS":"8F2397930A50C3C176188526EB9612578F9CBCE5"},"GK4":{"16:9 Widescreen":"4652969A4DA869FD28F2CAA3869A38F5C2AAFEB1"},"GKB":{"16:9 Widescreen Region-Free":"4652969A4DA869FD28F2CAA3869A38F5C2AAFEB1"},"GKBPAF":{"60Hz":"8E4E0ABA0E6D102A33206F34F4DEE63159B3CB4F"},"GKDP01":{"16:9 Widescreen":"DF4657937DE730107B636C73E959277EB963D210","60Hz":"6740D7B914CC897AF50798C0AB391965294FD1E1"},"GKRPB2":{"16:9 Widescreen":"69C01A4F91D991CF866237FED94A138308FC104F","60Hz":"40D06A5C7A3D873ABC73DEDF573738C2E3E37FF0"},"GKWJ18":{"16:9 Widescreen":"696570101FA040778EAD310377C484C846D87430"},"GKYE01":{"16:9 Widescreen":"6520EF1B7D88F00747B120A3A010458602ED989D"},"GKYJ01":{"16:9 Widescreen":"47B8EF7D02831AA5C375C698797BF2D5475FEFF2"},"GKYP01":{"16:9 Widescreen":"69413C75036D2975716066E6574461B981FF0124"},"GLEE08":{"Fix audio issues":"7355F358CAC6F418D37E4C23E64F7867D46E4FC9"},"GLEJ08":{"Fix audio issues":"12B24A6D7389A2AC5AB75FC0BF8493E7661F2A73"},"GLEP08":{"Fix audio issues":"81BD39F5527552DE89E3B59BA86298900F0A3168"},"GLSD64":{"Fix freeze in opening cutscene":"5E2A73717BD66EF647846DD64C33BC80AD9B5227"},"GLSE64":{"Fix freeze in opening cutscene":"1CE78E7954415A44DF693C0BB879AA5A4FF059A3"},"GLSF64":{"Fix freeze in opening cutscene":"009B0C4AD80A9C28C987934D254C2C4AACC9A07A"},"GLSP64":{"16:9 Widescreen":"8E7A544C10E7A5E2F0304A0D2586879627EF6586","60Hz":"B67144E87B54246137142992A3BC83DC13BE68A7","Fix freeze in opening cutscene":"3D0894616C9A7FA5ED91C1D2F461BF14DF47ECEC"},"GM4E01":{"16:9 Menu Backgrounds Add-On":"87D1A6A3E29ADA03B0D29C2F1841C18E62DB0A15","16:9 Widescreen v2":"78BE50F93E81A1972CA31ABC318949E366E82BD0"},"GM4P01":{"16:9 Menu Backgrounds Add-On":"87D1A6A3E29ADA03B0D29C2F1841C18E62DB0A15","16:9 Widescreen v2":"6D950B6EEFD6D304E1A424355B74A595D32BDF2F"},"GMBE8P":{"16:9 Widescreen":"D12DE9E3941BCE6EB50B8DA92140E09AFC0104C8","NA Stretched HUD":"0151A6683E1FBFD20096807C54A6952C4A4BAD40"},"GMBP8P":{"16:9 Widescreen":"8A688981F33A2C53882EF08FCF20B88AD43EF417"},"GMNE78":{"Limit internal FPS to VPS (improves performance)":"DE38BC8493EA24F93B6D805F28746AA99C53C677"},"GMNP78":{"Limit internal FPS to VPS (improves performance)":"65FB9B430F7D58EBC76DAADE8CA9DBBAA32B8B51"},"GMOP70":{"16:9 Widescreen":"CE4D298616BD42E4D8F8936B325CD1FD7F5B956B","60Hz":"0CD2CC787A5FF551901E41C85C8AAC02017ECB89"},"GMPE01":{"16:9 Widescreen":"4F98592DB3DEE3857469A8D8605FAF4BD6F7C76D","QOL - Allow Digital Presses for Map Screen":"E232B27564E9AA0C32DE163C9C056317A7B2B12E","QOL - Automatically Advance Text Boxes":"59607671BFC4717ACAF9807BB7EB0D9F982866D4","QOL - Disable Advance on Results":"CAEA37F3FEF89400513353EB85875F2A3AF4C03E","QOL - Faster Boot Time":"F75FBBD838C5B84FF687962FBA9195B217DE132C","QOL - Increased Board Speed":"19F264DE7F07EAC9433CA4B591BEBA1EC976C9F1","QOL - Increased Taunt Capabilities":"C70391D4961A0E820DE40141C89341369A9F021B","QOL - Instant Text Display":"F4E6913CF034E5778B9F9CA5FF448FF1B78B3333","QOL - Rumble Always Off":"93621947019532F02C25937FD3EEEE21A788CB07","QOL - Show Controller Port Number of Who Paused":"D7249AC3C949FEB860CF4350C5B7B79AC16569F1"},"GMPJ01":{"16:9 Widescreen":"A795811F2F0C92D9DCABDE97D9E39B47B1DBCEFD"},"GMPP01":{"16:9 Widescreen":"A795811F2F0C92D9DCABDE97D9E39B47B1DBCEFD"},"GMSE01":{"60FPS":"2805B1A0DD53EB64877D375D10F667700955720F","Widescreen":"BD718F961DBA5372B1D0257D454D535746C453A0"},"GMSJ01":{"16:9 Widescreen":"E8112A01040A06460E368F142C5D1FD0B4085D94"},"GMSJ01r0":{"60FPS":"9894B5B6B5215844D48411021FF8EECFE851D79D"},"GMSJ01r1":{"60FPS":"2DB69DB7A3753D543CD85CA4C77B4EF0AED7486E"},"GMSP01":{"60FPS":"D07009B710B3BBC6B12E54ED6A7969D58197EDAF","Widescreen":"0B7B89BF0868393076EF5F9DBD689DD0EDBCB84C"},"GNHE5d":{"Nop Hack":"89393A24E2336841AA4CD0AD3BE1C9A66B89E9EF"},"GOCE5D":{"16:9 Widescreen":"E4D800B90F16A15D9252EED1B2A23AEAF5CD230A"},"GOME01":{"16:9 Widescreen":"C33CBBF6F22195BF626104B4827200B3EE1CF30C"},"GOMP01":{"16:9 Widescreen":"874784CCD4AC3DC9C69654E471C18AD0F5E4D9AE"},"GONE69":{"Widescreen Hack Culling":"6412A67E1C26E146A9526AFC9F0EE517B214069D"},"GP4J18":{"16:9 Widescreen":"BF4D2D6AF66F285E81173B3B20D5369DB6FB351B"},"GP5E01":{"16:9 Widescreen":"5ED053787332DB5EEB8B963BA5DF197B58C6BB75","QOL - Automatically Advance Text Boxes":"41EF84663008668682BB75DC8B874E38669AE041","QOL - Disable Advance on Results":"6C36BEFE751131298BC8B39012892F6A6903CE60","QOL - Faster Boot Time":"8E952E6E12639AC20F6F9FDD150C12F0C294DF43","QOL - Increased Board Speed":"4BD72B8ED75ED8EFB3329C104BAD114FDDB8AD2C","QOL - Increased Capsule Throwing Speed":"C5BA3C9D386D09DD6CB9242A12A0D041BEB93160","QOL - Increased Taunt Capabilities":"C360EBDDCB0237076052C983BD9719411CA67CDE","QOL - Instant Text Display":"2CD4A06A3B9C18CC9EA2A2DEBD96E066CD1D700A","QOL - Show Controller Port Number of Who Paused":"5BA3648D18346EDC97B23475D4748C637E6095A2"},"GP5J01":{"16:9 Widescreen":"81E69B6BEE85E06805EC24E1CFEA9405BF459DF8"},"GP5P01":{"16:9 Widescreen":"FD4FCCB97C06F0173A30D4CC9A99422B2C0ABA9B"},"GP6E01":{"16:9 Widescreen":"B502AF0887792E9B140516D14BA75F99772C17BE","QOL - Auto Advance Text Boxes":"E645DF70CE0266D174D917A817A7FBEEE974A9F0","QOL - Disable Advance on Results":"798E566B621F6A32BF23C588E76EAC0776E2A8A3","QOL - Faster Boot Time":"B089A3C80D5DA86ABF6258F8BFB1DC78BB54A973","QOL - Increased Board Speed":"501C52ACF117950888A66DB2BEDBDFD7D9B20235","QOL - Increased Orb Throwing Speed":"845F439278DA8CBC225658FF4E2F82D707422BFB","QOL - Increased Taunt Capabilities":"0AED27341A06186AD92D5725C0B9693CA8F85B7B","QOL - Instant Text Display":"5AAC4E21927D4AEE7AC329CE793D4CDAEDDF7A3F","QOL - Show Controller Port Number of Who Paused":"DF5D2F068D76ABF2B1C294D413B9154B8C356929"},"GP6J01":{"16:9 Widescreen":"88F50F8298F82E3E5C161277BD4A985CE893A9D0"},"GP6P01":{"16:9 Widescreen":"DCCE59CA987624C753F0A86BEBA4287635901C93","Remove Black Bars":"138A86D43B5830BFE1926F58C0A4FBE2971BB02D"},"GP7E01":{"16:9 Widescreen":"87EBF72D90503EBC5CE9BC994795669E48A92A4A","QOL - Auto Advance Text Boxes":"82338AB5D82A7F398B4EB674CA4866DD1F4A1E65","QOL - Controller Options Always Acesssible":"D2F9CC0B9E52136E77187395FB7DA9B64B2C3B30","QOL - Disable Advance on Results":"A644551162A61DE7997B2A84F5BFDEFFD3FC2FB5","QOL - Faster Boot Time":"1939B5C575AE8FF6890BDDDD1B3FEA472D8CABA0","QOL - Increased Board Speed":"7E06A2EBCFECADB8074152B489984237A6FE9F21","QOL - Increased Orb Throwing Speed":"6671BEE71920D6B845AE1257AFE3AB3B95CD6FBD","QOL - Increased Taunt Capabilities":"9F60923F95FFE709CCD06966CD2DF743263934D1","QOL - Instant Text Display":"48FA6002E42DA9BB83ABC980A32C60CE3B21C4FC","QOL - Show Controller Port Number of Who Paused":"5E6F8C6C87D5944DB18A4E4F9A45F61EE87A4163"},"GP7J01":{"16:9 Widescreen":"A081F0729068D233E99DC00C64E36CA33C945640"},"GP7P01":{"16:9 Widescreen":"75D6CDA4EA301A71F7701A0487544E79136850B9","60Hz":"6D0D5B7CA58C38A3147F8ED981B5B21CC6C519CD"},"GPIE01":{"No Blur":"A25CDDB79991F090BD7F12840AD06F13BD5CCF95"},"GPIE01r0":{"16:9 Widescreen":"2B58166A66644F984E64077A6245C94C79B51063"},"GPIE01r1":{"16:9 Widescreen":"9A1545AB4E2B9216B95AFFA83420D723512DA8C8","60FPS":"3337C767EAA21D30C6CFEAE9985FFCABFC63E2E3","Disable Pikmin optimizations":"CBD15E9104929F0631713AE482475CB2B90E71F2"},"GPIP01":{"16:9 Widescreen":"5AE44D5B0E0D4C412B453B95CF5A41DBA4D685E6","60Hz":"AE59FF751E167632AE283F38CC4B96BDF7B9B81F","Turn off blur":"4ED79A548DBD7DBD35928A2F1138315FF103E260"},"GPNE08":{"16:9 Widescreen [Ralf, darkludx]":"ED3D9BB08C5F1D21BC8D5C73155C00E3C25C466F"},"GPNP08":{"16:9 Widescreen [Ralf]":"469B18FE8219031D355557B836EF02B625040A4F"},"GPOE8P":{"Make Save Copyable NTSC Port - 1.0\/1.1":"7A4B3E596BA4FDA8F3FCF984079B3F44CB2D6AB3","Make Save Copyable NTSC Port - PLUS\/1.2":"304DB311F2107E88EBCE058535B266F9263AA8E2","Save Validation Code v 1.0\/v1.1":"9F6259132453DCC0607EAB35DB182C76818F2F9F","Save Validation Code v1.2\/Plus":"17EB6CFB408EF27D44C053A1336C3B87B6A05018"},"GPOE8Pr0":{"16:9 Widescreen":"99986BB1D4ABE45C429D48B01BFB578FEA69C14F"},"GPOE8Pr1":{"16:9 Widescreen (Plus) [Ralf]":"5563671A6702785FFE106DAB7EF3F4FE6D9F705B"},"GPOP8P":{"16:9 Widescreen":"DF133C61C29D5CFA87A2A785C962C205F80E6D04","Game Save Valid":"40EC1590DB26C7C58D00E3065F1E404341EF0E73","Game Save copyable":"2F5FB98927DAFF141D1CCD1EACA8AF626397E284"},"GPVE01":{"16:9 Widescreen":"E5B51080CFD518FE584CF3B61A1099CD032479D6","60 FPS":"ADF5E3BBF0002A76949D0C11C51E58089BACFEED"},"GPVP01":{"16:9 Widescreen":"78F3C9A789827B063A6701987262276A66ABD82B"},"GQPE78":{"EFB Copy Fix":"880B114E9A308084CAB92C004A9EE067B371C310"},"GQPP78":{"EFB Copy Fix":"5D9A14954AE8D639C9B254F3BA73A70F284BBC8D"},"GQSDAF":{"16:9 Widescreen":"CA82C44B2E8FC5C184E3FF935BC89661B7DB55D6","60Hz":"952165FD78543EB6D2F5230F7570B0513773D332","No Blur":"DEB7DF358423F0EF30C9FD49F4F14590836D52A6"},"GQSEAF":{"16:9 Widescreen":"48AFE1D32843C6B5AD4337A49F15FD447DF2A752","Remove Blur":"EF448CF1FA6A15EB2661EA5338703C13D52EF65B"},"GQSFAF":{"16:9 Widescreen":"76C30E1737CC5C3B5DE32532B5F9A5CD50B690F8","No Blur":"986BAE5103CDE9286D2D34FD28FE2540D0759AAB"},"GR2E52":{"16:9 Widescreen":"5F55CE7DA7E1E3261CF2FF88F34CDA63BA511003"},"GR2J52":{"16:9 Widescreen":"D3C6FA690BED07BFA65C2E1FCDE2FFA5AA0EEEE4"},"GR2P52":{"16:9 Widescreen":"D7BCADD4E76B5E3F7D9D463F55A2777528E37E94"},"GRNE52":{"16:9 Widescreen":"483D74C47AD1012606D591A226AB3144C7FB201C"},"GRNJ52":{"16:9 Widescreen":"949281B7221B20680B7BC29E4754B73A326EBE85"},"GRNP52":{"16:9 Widescreen":"11051B094FE5A8B2E73060BFF786C1588E36979A"},"GROE5Z":{"16:9 Widescreen":"FADBAB5EC280CF8ED4C9536A33F4184BA210C9B0"},"GRYE41":{"Disable Culling to Fix Rise and Shrine Hang":"AF0A575EB6071EAC0D2EC3D2EA30A23EB05A4192"},"GSAE01r0":{"viWidth 704 Aspect Ratio Fix":"0D529180B9B28BB06E21EDBDBA61C17EEF0FFB7F"},"GSAE01r1":{"viWidth 704 Aspect Ratio Fix":"CB15ED22625690A5DBE4607FE30160125421461D"},"GSAP01":{"viWidth 704 Aspect Ratio Fix":"749EDA62B1B73354599929F1B50E992E111C5493"},"GSNE8P":{"16:9 Widescreen":"CFDF3FA5B48B347E49E8E521D7BE47FCC9A5CBF4","60Hz Aspect Ratio Fix":"D90F66C0D46D9BEF27C8E13BC8804DB1F382C8B1"},"GSNJ8P":{"60Hz Aspect Ratio Fix":"98DF9D67DE77F66A4B09F25C5621805DEB40865B"},"GSNP8P":{"60Hz Aspect Ratio Fix":"D90F66C0D46D9BEF27C8E13BC8804DB1F382C8B1"},"GSO":{"16:9 Widescreen Region Free":"0D60D0593F6DA28B1236381B22082506B9533F0E"},"GSPE69":{"16:9 Widescreen":"80E18A8963F4EFD3C03494C95934452FCB6E521B"},"GSPP69":{"16:9 Widescreen":"162E87E9F5511CD82216811055836C11B374C45B","60Hz":"E5DF2DF34D2F0BDEE4205C891576511EC572C5DC"},"GSWE64":{"16:9 Widescreen":"6480326CCA8FF64A4868EC6E9FDE9F38819A75A1","Disable Dithering":"85F65E4B627641862C1A6091D29A3BB0626B04C3"},"GTEE01":{"16:9 Widescreen":"6B57EBCB9CC0ACE2EDA0F13E3F1F55F3FABC3A26"},"GTEP01":{"16:9 Widescreen":"72785784363C46180AF8E388348FD77667E7D382"},"GTOJAF":{"Remove Blur":"53A2EDF113F2ED7E54A16AA0E73FDB2C44C79DE5"},"GTRE78r0":{"Limit internal FPS to VPS (improves performance)":"42B85EFC5DEF8B243521EAD434DDCBDF95664CB1"},"GTRE78r1":{"Limit internal FPS to VPS (improves performance)":"ADB3A41B992EAB15AE484B2E575ACF3354415186"},"GTRJ8N":{"Limit internal FPS to VPS (improves performance)":"47DBEFDCF22ADC2AB148A54AC885FE19B3C07E3F"},"GTRP78":{"Limit internal FPS to VPS (improves performance)":"E8752E1B60DF76CD0CC7408074E742B0966BC904"},"GTZE41":{"16:9 Widescreen":"ECCE87E0006475C73E7D936AA0150BD20166F9EF"},"GTZP41":{"16:9 Widescreen":"62D8F159B93167171E0860D96C4BB32A5BF5648B"},"GU2D78":{"EFB Copy Fix":"CFF4C3F932B08732627572EDA1A0CD2D9C71AE0C"},"GU2F78":{"EFB Copy Fix":"CFF4C3F932B08732627572EDA1A0CD2D9C71AE0C"},"GU3D78":{"EFB Copy Fix":"8A0E3114862ADFE421874211BD6F5220AA425BF5"},"GU3X78":{"EFB Copy Fix":"E3303FDAE7ECA17A72EDC440C32D94648A6453A0"},"GU4Y78":{"EFB Copy Fix":"D54767785E139A8BC8C4B75573FBD5A0B686D8E3"},"GUNE5Dr0":{"16:9 Widescreen":"C942EBCBE8A487C25E296EC1FAC2358DA1487DBD","60 FPS":"A441630EC5FF0EB74D2243A3092D22E69C6BEA02"},"GUNE5Dr1":{"16:9 Widescreen":"39BD84CBE2AFACFEDEC5E9020D1A8B0D36DD53F9","60 FPS":"05779F1A3D0C43305D52A95447D612CB424942C0"},"GUNP5D":{"16:9 Widescreen":"FE3BA1DAA1AF278A7839E27BF466B0BA0B390EC2","50 FPS":"3B20499A3F8D1D7CCA5B0015F2D80ECED25842A6"},"GV4E69":{"Fix 2D Rendering":"8679891FCAA250FCFF670B26E0CB9875900D17FD"},"GVJE08":{"16:9 Widescreen":"09EFDA75D876A675121C8344D8FDD09A70A1A846"},"GVJP08":{"16:9 Widescreen":"93938F4C6112C91549F2B52E6259170F7501EC07"},"GVPE69":{"Fix 2D Rendering":"3159CA79B0A890131763EA6CB163684BEE886E3F"},"GVSE8P":{"16:9 Widescreen":"4CE9C491160A4B631142EE9CE802C694163F1CA3"},"GWRE01":{"16:9 Widescreen":"AC42770B06662BE1DC863EC80F44B5E034C63664"},"GWRP01":{"16:9 Widescreen":"9DDDFAB28C4BD35CF64050E1EF684DC042B1AFFA"},"GWWE01":{"16:9 Widescreen":"98B2E75D8E1CED4A964D3129A5DC10E30538CAA6"},"GWWJ01":{"16:9 Widescreen":"2DAD9A5E2A140F02CCBA727C4BE7C74BAC156778"},"GWWP01":{"16:9 Widescreen":"2DAD9A5E2A140F02CCBA727C4BE7C74BAC156778"},"GWZE01":{"16:9 Widescreen":"9EFC191DE6D21A1681FE241AB2EE4A131259F317"},"GXCE01":{"16:9 Widescreen":"901A1E78A3A0124F55548507D3B3707125C64A8A"},"GXSE8P":{"16:9 Widescreen":"F50BBA440184FC77A4DFFAA58FF2BB888E2E072F","Aspect Ratio Fix":"4214C74DFB8A74FC3AA4A643E869BC4D9A38EDA0"},"GXSJ8P":{"Aspect Ratio Fix":"227909607984BBC3D36AAC7DB9DFE385F3363C49"},"GXXE01":{"16:9 Widescreen":"7CAFDAD7B5E7459CCC1BB209D439DDCBB5BE4E4E","60 FPS":"7E2829049003FC9DC8BC28044B33CAFF3768B54C","Allow Memory Card saving with Savestates":"64FAA15062F0D0C319F904BBDE9C4489A25D6369"},"GXXJ01":{"16:9 Widescreen":"AF704A8C6838FDF47054D9EDD9106B3F46781FA9","60 FPS":"4F5FD45B4EC1B33FE076FA06F8FF863C46A06BBA","Allow Memory Card saving with Savestates":"8293802260536FA2EF2EFDAB5266DE36BB88DE1B"},"GXXP01":{"16:9 Widescreen":"02716B2585BEE74F8FCDD97A78F6A0D3DC7F331B","60 FPS":"BF6171EA5CCF4B57AA596372589DDB5E6904DD7A","Allow Memory Card saving with Savestates":"3CAFBC4AE6FC5CE9F53377F86AB5BD8F1BC8861A"},"GYFPA4":{"60Hz":"402ED10AC842041AB4B39AE8F2D81B2D7AEF9CB4"},"GZ2E01":{"16:9 Widescreen v2":"E7521ED27BFC972628906CBE8D5403ED57253BB8","Hyrule Field Speed Hack":"FCB673D46E716C7F63C618B8D8BF83AEE0B501F0"},"GZ2J01":{"16:9 Widescreen":"F985A0A58D8E2B23E8A557FAFF8D367AFFEADD07","Hyrule Field Speed Hack":"FCB673D46E716C7F63C618B8D8BF83AEE0B501F0"},"GZ2P01":{"16:9 Widescreen v2":"CAC38B0D334B925A57AA3118D35932B8A185137E","Hyrule Field Speed Hack":"0F63623D4D984B7706F718F57C0ABDB6DBADCF8D"},"GZ3E70":{"16:9 Widescreen":"167F9E9A9B372CB3A01F308B46FF1403F8599C51","Deinterlacing Fix":"EF1291C0E45277CDAE4D0F61E1949C1E75E5A3E1"},"GZBJB2":{"Deinterlacing Fix":"19D67BD12A18F0E3A960BBB92E2F490BEF5C00C1"},"GZDP70":{"16:9 Widescreen":"2E424E0BBAE6EF5D6A8FB4224EA1D0746BAC37D3"},"GZLE01":{"16:9 Widescreen":"9FA864EE7DD8CE7FF538EB4E0243F20137430BD8","Remove Distance Blur":"78EA34CEF8E01701491C280F155F1C12EC9BF1A2"},"GZLJ01":{"16:9 Widescreen":"E03B61989025CA33937E63A057E6E40A403811BC"},"GZLP01":{"16:9 Widescreen":"3EFFF6C52B5633A1729FAA6883D8579E77F7D057","Remove Distance Blur":"14BECBEA4DD281EBD0F7FE7DEE8020B6F2418ACD"},"GZPE70":{"16:9 Widescreen":"7142F2495507AC7136992128ED0FCA6BC72B61F4"},"GZPP70":{"16:9 Widescreen":"591FD6C9668C79FF1CE8558EBED2486A7A327F05","60Hz":"A59B84DB5486521AEE1C23B6C741ECD35216E5AD"},"GZSE70":{"16:9 Widescreen":"8E86EAC7EA4F4D2854DD9020CD795630CA64C4EE"},"HAF":{"BufferPatch":"181195871F63B89B1CF09AFA4420CF89B9883108"},"HAL":{"RSAPatch":"AD12237401ABE9FE4A545AADB5C5AE10355E2076"},"R7XE69":{"Speed hack":"53F3273525BB719D535007FB97E7F381D808A8A9"},"R7XJ13":{"Speed hack":"4590D425BCBAFA0D1E32D27B46068843F6A2BE09"},"R7XP69":{"Speed hack":"53F3273525BB719D535007FB97E7F381D808A8A9"},"RB7E54":{"Limit internal frame rate in loading screens (fix hangs)":"BD1A252B43FD339391ED33554542E786BAD6671B"},"RB7P54":{"Limit internal frame rate in loading screens (fix hangs)":"90247F89DF8EB7A4AFA2C425CD3006257A95A8F8"},"RGQE70":{"crashfix":"5F4CF8D4DA19A0FF74FF9EB925AC0236069BFD59"},"RLEEFS":{"Fix crash on main menu":"793642AC6862C2F3412035A9E3D7172CC4A1D5C7"},"RM8E01":{"16:9 Widescreen":"E61344EB1542A78D497981C307B6549985C7A05A","Extra - Disable Music":"25223F9EFAABF601CAC7810004F124E4056598B1","QOL - Faster Boot Time":"BCC4279F8B28636AD773F01540E78DF40EAD6087","QOL - Increased Board Speed":"B65AF1819966CD3435D88801E8C79704E3A52DB5","QOL - Increased Taunt Capabilities":"66495D7CB532FAE778AFC22CF45D17D0FFDE5310","QOL - Increased Text Display":"B4186DDC54F33F4D6A22188EF50CEB43FB205673","QOL - Invert IR Stick for GameCube Mod":"9D90A9C66AE8AD91B201B40C4145D1323B701A77","QOL - Remove Explanations":"330DD53AB993A99576564FEFD222D7BD211B878F"},"RM8J01":{"16:9 Widescreen":"A140BDCB8E1721CB6B4CD878E412113322258B57"},"RM8P01":{"16:9 Widescreen":"F664C32AFD3D785FC6E04D8990A3FA1C72A18C5C"},"RMHE08":{"Bloom OFF":"CCF233DA57B3E75221870DE502955114B0D4E7FA"},"RMHJ08":{"Bloom OFF":"29D3625B7ED577587E56AA07CB0EB8C47C97E823"},"RMHP08":{"Bloom OFF":"1720C1173D4698167080DBFC4232F21757C4DA08"},"RO2P7N":{"Hangfix":"EEE9C8DE4671C18DD7F81DD08D39B64C57600DEA"},"RPBE01":{"Fix black screen effects":"775ABECA6073E02C5C68CF4D644194D966A418F5"},"RPBJ01r0":{"Fix black screen effects":"0EAB5D8DE827894AFEF97C10ACB67378E6983323"},"RPBJ01r1":{"Fix black screen effects":"4905E08643E9D00136F7EAF51978CF2F54D10D07"},"RPBJ01r2":{"Fix black screen effects":"4905E08643E9D00136F7EAF51978CF2F54D10D07"},"RPBP01":{"Fix black screen effects":"82AEB60F9A9083F93060531A970FFAABE0833A40"},"RRBE41r0":{"Idle loop speed hack":"20C7A2D4A3D6D55A8029A9D200D57CBD11F60B88"},"RRBE41r1":{"Idle loop speed hack":"0A30FA4C7E787197A00B7267368F372EFF623776"},"RRBE41r2":{"Idle loop speed hack":"2227787007F16F0CD454422AE6EB94656464B6D0"},"RRBJ41":{"Idle loop speed hack":"E9209EB728AA13375B2ABEF1C15D066DAFBF2EBF"},"RRBP41":{"Idle loop speed hack":"8A47BC311138EE3A11E049DF3EE6AED38990FB49"},"RTH":{"Disable blur":"812EE46AC967BFCD239335B10A664D71A93E8175"},"RUUE01r0":{"QOL - Classic Controller Support (Pointer on Left Stick, recommended) v1.1":"244D1FC7D6284DC8832CF7F34506B0DE4DBF8914","QOL - Classic Controller Support (Pointer on Right Stick) v1.1":"6FDD09D51EC3D754BA7F80630B1FE1D3FEF6FE97","QOL - Disable Grass Deterioration":"D566F2AF5BB1EDB8284461DEA85FF3B5A8DEBDD6"},"RUUE01r1":{"QOL - Classic Controller Support (Pointer on Left Stick, recommended) v1.1":"0235631B6F9A439B9718635F5AB7ADAFA2B6B3DE","QOL - Classic Controller Support (Pointer on Right Stick) v1.1":"5DEAC5F3390CE19487A9473A1988E7BB5EA4790F"},"RUUJ01r1":{"QOL - Classic Controller Support (Pointer on Left Stick, recommended) v1.1":"C88CFD7E6C18F570DE974D6C0202987E4F1B72C4","QOL - Classic Controller Support (Pointer on Right Stick) v1.1":"3C92010E7DE2E93C48AFF25B30A18BBFCBD12707"},"RUUK01r1":{"QOL - Classic Controller Support (Pointer on Left Stick, recommended) v1.1":"EF883FB0C237BF51ABD88C35D34571D240C79484","QOL - Classic Controller Support (Pointer on Right Stick) v1.1":"F08D0F233F3F5833DFAFE5C8049C37D32360F0FA"},"RUUP01r0":{"QOL - Classic Controller Support (Pointer on Left Stick, recommended) v1.1":"D28D4E4FBD153A6D4C49E2D250213F37BEC941E5","QOL - Classic Controller Support (Pointer on Right Stick) v1.1":"80C3F50C018E076DF2CD39732A5ABB8A1C0E88B1","QOL - Disable Grass Deterioration":"3B278C19AC76D27488C55201BA0DB63864F66AAF"},"RUUP01r1":{"QOL - Classic Controller Support (Pointer on Left Stick, recommended) v1.1":"9121BEDD72436F4EAB47E4CE96ABD864118B22F3","QOL - Classic Controller Support (Pointer on Right Stick) v1.1":"8FC58B3DC4817515822EBB82BB7A8055270EE51D","QOL - Disable Grass Deterioration":"9B199D77AF748518358D32966F3F55F5C5759C3A"},"RX4E4Z":{"Fix file reads (dcache bypass)":"9E4E0F1465A9A1E85349DBA3B1278AC215A97DBB"},"RX4PMT":{"Fix file reads (dcache bypass)":"EE85907C03F0295794821383B93F8D5B91D2697A"},"RZDE01r0":{"Hyrule Field Speed Hack":"15EAD073414C9903D6CAE5229DCE582BD17A9162"},"RZDE01r2":{"Hyrule Field Speed Hack":"27395CC8BC2C51201D566657D31A471A850482FB"},"RZDJ01":{"Hyrule Field Speed Hack":"B3F7473F8C911A32F1D616491C9E78EBBD7A6309"},"RZDK01":{"Hyrule Field Speed Hack":"A280C0114B800D7DC056ECFB5E482229DA0B1550"},"RZDP01":{"Hyrule Field Speed Hack":"2A83ADFB760F9498841ED0ED68B0C0438232472C"},"SAOE78":{"Fix crash on boot":"EA11FA4908FB20B61876ACD360EC7657A6D39FB2"},"SAOEVZ":{"Fix crash on boot":"AA55C214DE7545DE0E203CC39F06BF3D31451BE9"},"SC2":{"Limit internal frame rate (speed hack)":"B3FB0FCE108E85E6198BAF3535850470049E2BE2"},"SGLEA4":{"Fix black screen":"258378187ACF475A55EFEAF8A703681252E014C3"},"SGLPA4":{"Fix black screen":"6F8CD59D897338CA90939149E1A62588620C6D88"}} \ No newline at end of file
diff --git a/Data/Sys/GameSettings/G3L.ini b/Data/Sys/GameSettings/G3L.ini
index 279371250b..7d7b95bf79 100644
--- a/Data/Sys/GameSettings/G3L.ini
+++ b/Data/Sys/GameSettings/G3L.ini
@@ -1,4 +1,4 @@
-# G3LE8P - Super Monkey Ball Adventure
+# G3LE8P, G3LP8P - Super Monkey Ball Adventure
[Core]
# Values set here will override the main Dolphin settings.
diff --git a/Data/Sys/GameSettings/G4S.ini b/Data/Sys/GameSettings/G4S.ini
index f469b42057..7b1bd52158 100644
--- a/Data/Sys/GameSettings/G4S.ini
+++ b/Data/Sys/GameSettings/G4S.ini
@@ -1,4 +1,4 @@
-# G4SE01, G4SP01 - The Legend of Zelda: Four Swords
+# G4SE01, G4SP01 - The Legend of Zelda: Four Swords Adventures
[Core]
# Values set here will override the main Dolphin settings.
diff --git a/Data/Sys/GameSettings/G4SP01.ini b/Data/Sys/GameSettings/G4SP01.ini
index 261a3236e3..834c6239ca 100644
--- a/Data/Sys/GameSettings/G4SP01.ini
+++ b/Data/Sys/GameSettings/G4SP01.ini
@@ -1,4 +1,4 @@
-# G4SP01 - The Legend of Zelda: Four Swords
+# G4SP01 - The Legend of Zelda: Four Swords Adventures
[OnFrame]
# Add memory patches to be applied every frame here.
diff --git a/Data/Sys/GameSettings/GEYJ6E.ini b/Data/Sys/GameSettings/GEYJ6E.ini
new file mode 100644
index 0000000000..c410c05bd7
--- /dev/null
+++ b/Data/Sys/GameSettings/GEYJ6E.ini
@@ -0,0 +1,4 @@
+# GEYJ6E - Virtua Striker 2002 (Japan)
+
+[Core]
+FPRF = True
diff --git a/Data/Sys/GameSettings/GEYP6E.ini b/Data/Sys/GameSettings/GEYP6E.ini
new file mode 100644
index 0000000000..ccfcf0a9e4
--- /dev/null
+++ b/Data/Sys/GameSettings/GEYP6E.ini
@@ -0,0 +1,4 @@
+# GEYP6E - Virtua Striker 2002 (Export, Type3)
+
+[Core]
+FPRF = True
diff --git a/Data/Sys/GameSettings/GGGE6E.ini b/Data/Sys/GameSettings/GGGE6E.ini
new file mode 100644
index 0000000000..8391b80aca
--- /dev/null
+++ b/Data/Sys/GameSettings/GGGE6E.ini
@@ -0,0 +1,5 @@
+# GGGE6E - F-ZERO AX
+
+[Core]
+FPRF = True
+
diff --git a/Data/Sys/GameSettings/GHAE6E.ini b/Data/Sys/GameSettings/GHAE6E.ini
new file mode 100644
index 0000000000..4f6821b253
--- /dev/null
+++ b/Data/Sys/GameSettings/GHAE6E.ini
@@ -0,0 +1,5 @@
+# GHAE6E - F-ZERO AX (Monster)
+
+[Core]
+FPRF = True
+
diff --git a/Data/Sys/GameSettings/GHZW6E.ini b/Data/Sys/GameSettings/GHZW6E.ini
new file mode 100644
index 0000000000..afd22fb56c
--- /dev/null
+++ b/Data/Sys/GameSettings/GHZW6E.ini
@@ -0,0 +1,4 @@
+# GHZW6E - VIRTUA STRIKER 4 (Asia)
+
+[Core]
+FPRF = True
diff --git a/Data/Sys/GameSettings/GJAP6E.ini b/Data/Sys/GameSettings/GJAP6E.ini
new file mode 100644
index 0000000000..071cb0d5b9
--- /dev/null
+++ b/Data/Sys/GameSettings/GJAP6E.ini
@@ -0,0 +1,4 @@
+# GJAP6E - VIRTUA STRIKER 4 (Export)
+
+[Core]
+FPRF = True
diff --git a/Data/Sys/GameSettings/GLKJ6E.ini b/Data/Sys/GameSettings/GLKJ6E.ini
new file mode 100644
index 0000000000..9b981daee6
--- /dev/null
+++ b/Data/Sys/GameSettings/GLKJ6E.ini
@@ -0,0 +1,4 @@
+# GLKJ6E - VIRTUA STRIKER 4 Ver.2006 (Japan)
+
+[Core]
+FPRF = True
diff --git a/Data/Sys/GameSettings/GLLP6E.ini b/Data/Sys/GameSettings/GLLP6E.ini
new file mode 100644
index 0000000000..b607f28de4
--- /dev/null
+++ b/Data/Sys/GameSettings/GLLP6E.ini
@@ -0,0 +1,4 @@
+# GLLP6E - VIRTUA STRIKER 4 Ver.2006 (Export)
+
+[Core]
+FPRF = True
diff --git a/Data/Sys/GameSettings/GWWE01.ini b/Data/Sys/GameSettings/GWWE01.ini
index ad8ba01bef..1fd3ee9fc9 100644
--- a/Data/Sys/GameSettings/GWWE01.ini
+++ b/Data/Sys/GameSettings/GWWE01.ini
@@ -1,4 +1,4 @@
-# GWWE01 - WARIO WORLD
+# GWWE01 - Wario World
[OnFrame]
# Add memory patches to be applied every frame here.
diff --git a/Data/Sys/GameSettings/GWWJ01.ini b/Data/Sys/GameSettings/GWWJ01.ini
index 1568017485..94ee2bacae 100644
--- a/Data/Sys/GameSettings/GWWJ01.ini
+++ b/Data/Sys/GameSettings/GWWJ01.ini
@@ -1,4 +1,4 @@
-# GWWJ01 - WARIO WORLD
+# GWWJ01 - Wario World
[OnFrame]
# Add memory patches to be applied every frame here.
diff --git a/Data/Sys/GameSettings/GWWP01.ini b/Data/Sys/GameSettings/GWWP01.ini
index 9975d3598a..86359fe4a4 100644
--- a/Data/Sys/GameSettings/GWWP01.ini
+++ b/Data/Sys/GameSettings/GWWP01.ini
@@ -1,4 +1,4 @@
-# GWWP01 - WARIO WORLD
+# GWWP01 - Wario World
[OnFrame]
# Add memory patches to be applied every frame here.
diff --git a/Data/Sys/GameSettings/GXRP08.ini b/Data/Sys/GameSettings/GXRP08.ini
new file mode 100644
index 0000000000..9086044276
--- /dev/null
+++ b/Data/Sys/GameSettings/GXRP08.ini
@@ -0,0 +1,6 @@
+# GXRP08 - Mega Man X: Command Mission
+
+[Gecko]
+$16:9 Widescreen
+045ACC34 C2D40000
+045ACC38 42D40000
diff --git a/Data/Sys/GameSettings/RMC.ini b/Data/Sys/GameSettings/RMC.ini
index c2bfa056c5..a2725cb5ca 100644
--- a/Data/Sys/GameSettings/RMC.ini
+++ b/Data/Sys/GameSettings/RMC.ini
@@ -10,7 +10,6 @@
[Video_Hacks]
EFBEmulateFormatChanges = True
-DeferEFBCopies = False
[Video_Stereoscopy]
StereoConvergence = 613
diff --git a/Data/Sys/GameSettings/RSM.ini b/Data/Sys/GameSettings/RSM.ini
index 397ec620ff..f890cfed58 100644
--- a/Data/Sys/GameSettings/RSM.ini
+++ b/Data/Sys/GameSettings/RSM.ini
@@ -1,4 +1,4 @@
-# RSME8P, RSMJ8P, RSMP8P - SUPER MONKEY BALL BANANA BLITZ
+# RSME8P, RSMJ8P, RSMP8P - Super Monkey Ball: Banana Blitz
[Core]
# Values set here will override the main Dolphin settings.
@@ -9,4 +9,3 @@ FPRF = True
[ActionReplay]
# Add action replay cheats here.
-
diff --git a/Data/Sys/GameSettings/RUUE01r0.ini b/Data/Sys/GameSettings/RUUE01r0.ini
new file mode 100644
index 0000000000..b5bbc61784
--- /dev/null
+++ b/Data/Sys/GameSettings/RUUE01r0.ini
@@ -0,0 +1,254 @@
+# RUUE01 - Animal Crossing: City Folk (NTSC-U, Rev 0)
+
+[Core]
+# Values set here will override the main Dolphin settings.
+
+[OnFrame]
+# Add memory patches to be applied every frame here.
+
+[ActionReplay]
+# Add action replay cheats here.
+
+[Gecko]
+# Add gecko cheats here.
+
+$!!! - README
+*It is required you configure your Wii Remote with a mapped Classic Controller extension!
+*Left Stick variant toggles between movement and IR input (recommended!).
+*Right Stick variant allows cursor movement while moving, but this is not a real advantage and may feel less convenient.
+*Pointer mode is activated intelligently, but can be forced on/off with the L button.
+*3C75C28F (float 0.015) sets cursor speed. You can adjust it between 3C23D70A (0.01) and 3CA3D70A (0.02).
+*To use the in-game keyboard, it is recommended to connect a real keyboard over using the pointer.
+*More information: https://gbatemp.net/threads/new-classic-controller-hacks.659837/post-10507857
+
+$!!! - CONTROL SCHEME
+*Remote & Nunchuk Classic Controller Game Function
+*--------------------------------------------------------------------
+*Remote Home Home Open/Close Home Menu
+*Remote D-Pad D-Pad Up: Change Camera
+* --- --- Left/Right: Change Tool
+* --- --- Down: Put Away Tool
+*Remote A A Confirm, Action
+*Remote B B Cancel, Run, Pick Up Item
+*Remote + Plus Open/Close Map
+* --- ZR Next Tab
+*Remote - X Open/Close Inventory
+* --- ZL Previous Tab
+*Remote 1 Minus Take Photo
+*Remote 2 Y Open/Close Photos
+*Nunchuk Stick Left Stick Movement
+*Nunchuk C N/A (Use A) Confirm, Action
+*Nunchuk Z R Cancel, Run, Pick Up Item
+*Remote Pointer L (Toggle) Move Cursor
+*
+*For the Pointer, use Left or Right Stick, depending on code used.
+*Home Menu works as normal, but remember to enable the pointer with L.
+
+$QOL - Classic Controller Support (Pointer on Left Stick, recommended) v1.1 [Vague Rant, crediar]
+C20FA624 00000003
+2C030002 40820008
+3863FFFF 3803FF04
+60000000 00000000
+C20FA68C 00000003
+2C040002 40820008
+3884FFFF 20640001
+60000000 00000000
+042CE160 48000010
+C2443CE4 00000003
+2C000001 4182000C
+C0230104 48000008
+C0230074 00000000
+C2443D00 00000003
+2C000001 4182000C
+C0230108 48000008
+C0230078 00000000
+C23BE750 0000001C
+88A40028 2C050002
+408200CC 88A3005E
+2C050002 4C820020
+9421FFF0 7C0802A6
+9001000C 90610008
+3CC0803A 60C6D338
+7CC803A6 4E800021
+2C030001 80610008
+48000005 7CA802A6
+C0450084 40820008
+EC4200B2 C0650088
+C0030020 C023006C
+FC211024 48000039
+D0030020 C0030024
+C0230070 FC200850
+48000025 D0030024
+38000000 9003006C
+90030070 8001000C
+7C0803A6 38210010
+4E800020 FC0100FA
+C025008C FC000800
+4180000C FC000890
+48000014 FC200850
+FC000800 41810008
+FC000890 4E800020
+3FAAAAAB 3C75C28F
+3F800000 9421FFC0
+60000000 00000000
+C23BF784 00000026
+901F0068 811F0060
+48000061 80DF0000
+7D083378 911F0000
+7D0840F8 80DF0008
+7CC84038 911F0008
+811F0064 4800003D
+80DF0004 7D083378
+911F0004 71060040
+41820010 38C00002
+98DF005E 480000E0
+71060080 418200D8
+88DF005E 68C60002
+98DF005E 480000C8
+38E00000 71060800
+41820008 60E78000
+71060001 41820008
+60E70008 71064000
+41820008 60E70004
+71060002 41820008
+60E70001 71068000
+41820008 60E70002
+71060010 41820008
+60E70800 71060040
+41820008 60E70400
+71060008 41820008
+60E71080 71060020
+41820008 60E70180
+71062000 41820008
+60E70080 71060200
+41820008 60E72000
+71060080 41820008
+60E71040 71060004
+4182000C 60E70050
+48000004 71060400
+41820008 60E70090
+71061000 41820008
+60E70200 7CE83B78
+4E800020 00000000
+*Left Stick variant toggles between movement and IR input (recommended!).
+*See README and CONTROL SCHEME for further instructions.
+
+$QOL - Classic Controller Support (Pointer on Right Stick) v1.1 [Vague Rant, crediar]
+C20FA624 00000003
+2C030002 40820008
+3863FFFF 3803FF04
+60000000 00000000
+C20FA68C 00000003
+2C040002 40820008
+3884FFFF 20640001
+60000000 00000000
+042CE160 48000010
+C2443CE4 00000003
+2C000001 4182000C
+C0230104 48000008
+C0230074 00000000
+C2443D00 00000003
+2C000001 4182000C
+C0230108 48000008
+C0230078 00000000
+C23BE750 0000001A
+88A40028 2C050002
+408200C0 88A3005E
+2C050002 4C820020
+9421FFF0 7C0802A6
+9001000C 90610008
+3CC0803A 60C6D338
+7CC803A6 4E800021
+2C030001 80610008
+48000005 7CA802A6
+C0450078 40820008
+EC4200B2 C065007C
+C0030020 C0230074
+FC211024 4800002D
+D0030020 C0030024
+C0230078 FC200850
+48000019 D0030024
+8001000C 7C0803A6
+38210010 4E800020
+FC0100FA C0250080
+FC000800 4180000C
+FC000890 48000014
+FC200850 FC000800
+41810008 FC000890
+4E800020 3FAAAAAB
+3C75C28F 3F800000
+9421FFC0 00000000
+C23BF784 00000026
+901F0068 811F0060
+48000061 80DF0000
+7D083378 911F0000
+7D0840F8 80DF0008
+7CC84038 911F0008
+811F0064 4800003D
+80DF0004 7D083378
+911F0004 71060040
+41820010 38C00002
+98DF005E 480000E0
+71060080 418200D8
+88DF005E 68C60002
+98DF005E 480000C8
+38E00000 71060800
+41820008 60E78000
+71060001 41820008
+60E70008 71064000
+41820008 60E70004
+71060002 41820008
+60E70001 71068000
+41820008 60E70002
+71060010 41820008
+60E70800 71060040
+41820008 60E70400
+71060008 41820008
+60E71080 71060020
+41820008 60E70180
+71062000 41820008
+60E70080 71060200
+41820008 60E72000
+71060080 41820008
+60E71040 71060004
+4182000C 60E70050
+48000004 71060400
+41820008 60E70090
+71061000 41820008
+60E70200 7CE83B78
+4E800020 00000000
+*Right Stick variant allows cursor movement while moving, but this is not a real advantage and may feel less convenient.
+*See README and CONTROL SCHEME for further instructions.
+
+$QOL - Disable Grass Deterioration [Seth@WiiPlaza, Fox_McCloud45]
+042B62EC 60000000
+*Prevents grass from deteriorating when walked on.
+*Hides deteriorating grass patches, e.g., around houses.
+*Does not modify pre-existing save file data; grass will still be deteriorated when disabled.
+*While active, save file data remains static; grass will neither heal nor deteriorate in save data.
+
+$QOL - Set Grass Type to Green Darkest (Rain) [Seth@WiiPlaza, hetoan2]
+04162974 38600000
+$QOL - Set Grass Type to Green Lightest (Rain) [Seth@WiiPlaza, hetoan2]
+04162974 38600005
+$QOL - Set Grass Type to Brown w/ Green (Rain) [Seth@WiiPlaza, hetoan2]
+04162974 38600006
+$QOL - Set Grass Type to Brown Lightest (Rain) [Seth@WiiPlaza, hetoan2]
+04162974 38600007
+$QOL - Set Grass Type to Brown Darkest (Rain) [Seth@WiiPlaza, hetoan2]
+04162974 38600010
+$QOL - Set Grass Type to Brown Ground Light (Snow) [Seth@WiiPlaza, hetoan2]
+04162974 38600011
+$QOL - Set Grass Type to Brown Ground Dark (Snow) [Seth@WiiPlaza, hetoan2]
+04162974 38600012
+$QOL - Set Grass Type to Snowy w/ Purple/Brown Trees(Snow) [Seth@WiiPlaza, hetoan2]
+04162974 38600013
+$QOL - Set Grass Type to Snowy w/ Blue Trees (Snow) [Seth@WiiPlaza, hetoan2]
+04162974 38600014
+$QOL - Set Grass Type to Snowy w/ Green Trees (Snow) [Seth@WiiPlaza, hetoan2]
+04162974 38600015s
+
+[Gecko_RetroAchievements_Verified]
+$QOL - Classic Controller Support (Pointer on Left Stick, recommended) v1.1
+$QOL - Classic Controller Support (Pointer on Right Stick) v1.1
+$QOL - Disable Grass Deterioration \ No newline at end of file
diff --git a/Data/Sys/GameSettings/RUUE01r1.ini b/Data/Sys/GameSettings/RUUE01r1.ini
new file mode 100644
index 0000000000..279c9bc96c
--- /dev/null
+++ b/Data/Sys/GameSettings/RUUE01r1.ini
@@ -0,0 +1,225 @@
+# RUUE01 - Animal Crossing: City Folk (NTSC-U, Rev 1)
+
+[Core]
+# Values set here will override the main Dolphin settings.
+
+[OnFrame]
+# Add memory patches to be applied every frame here.
+
+[ActionReplay]
+# Add action replay cheats here.
+
+[Gecko]
+# Add gecko cheats here.
+
+$!!! - README
+*It is required you configure your Wii Remote with a mapped Classic Controller extension!
+*Left Stick variant toggles between movement and IR input (recommended!).
+*Right Stick variant allows cursor movement while moving, but this is not a real advantage and may feel less convenient.
+*Pointer mode is activated intelligently, but can be forced on/off with the L button.
+*3C75C28F (float 0.015) sets cursor speed. You can adjust it between 3C23D70A (0.01) and 3CA3D70A (0.02).
+*To use the in-game keyboard, it is recommended to connect a real keyboard over using the pointer.
+*More information: https://gbatemp.net/threads/new-classic-controller-hacks.659837/post-10507857
+
+$!!! - CONTROL SCHEME
+*Remote & Nunchuk Classic Controller Game Function
+*--------------------------------------------------------------------
+*Remote Home Home Open/Close Home Menu
+*Remote D-Pad D-Pad Up: Change Camera
+* --- --- Left/Right: Change Tool
+* --- --- Down: Put Away Tool
+*Remote A A Confirm, Action
+*Remote B B Cancel, Run, Pick Up Item
+*Remote + Plus Open/Close Map
+* --- ZR Next Tab
+*Remote - X Open/Close Inventory
+* --- ZL Previous Tab
+*Remote 1 Minus Take Photo
+*Remote 2 Y Open/Close Photos
+*Nunchuk Stick Left Stick Movement
+*Nunchuk C N/A (Use A) Confirm, Action
+*Nunchuk Z R Cancel, Run, Pick Up Item
+*Remote Pointer L (Toggle) Move Cursor
+*
+*For the Pointer, use Left or Right Stick, depending on code used.
+*Home Menu works as normal, but remember to enable the pointer with L.
+
+$QOL - Classic Controller Support (Pointer on Left Stick, recommended) v1.1 [Vague Rant, crediar]
+C20FA624 00000003
+2C030002 40820008
+3863FFFF 3803FF04
+60000000 00000000
+C20FA68C 00000003
+2C040002 40820008
+3884FFFF 20640001
+60000000 00000000
+042CE284 48000010
+C2443F4C 00000003
+2C000001 4182000C
+C0230104 48000008
+C0230074 00000000
+C2443F68 00000003
+2C000001 4182000C
+C0230108 48000008
+C0230078 00000000
+C23BE9B8 0000001C
+88A40028 2C050002
+408200CC 88A3005E
+2C050002 4C820020
+9421FFF0 7C0802A6
+9001000C 90610008
+3CC0803A 60C6D5A0
+7CC803A6 4E800021
+2C030001 80610008
+48000005 7CA802A6
+C0450084 40820008
+EC4200B2 C0650088
+C0030020 C023006C
+FC211024 48000039
+D0030020 C0030024
+C0230070 FC200850
+48000025 D0030024
+38000000 9003006C
+90030070 8001000C
+7C0803A6 38210010
+4E800020 FC0100FA
+C025008C FC000800
+4180000C FC000890
+48000014 FC200850
+FC000800 41810008
+FC000890 4E800020
+3FAAAAAB 3C75C28F
+3F800000 9421FFC0
+60000000 00000000
+C23BF9EC 00000026
+901F0068 811F0060
+48000061 80DF0000
+7D083378 911F0000
+7D0840F8 80DF0008
+7CC84038 911F0008
+811F0064 4800003D
+80DF0004 7D083378
+911F0004 71060040
+41820010 38C00002
+98DF005E 480000E0
+71060080 418200D8
+88DF005E 68C60002
+98DF005E 480000C8
+38E00000 71060800
+41820008 60E78000
+71060001 41820008
+60E70008 71064000
+41820008 60E70004
+71060002 41820008
+60E70001 71068000
+41820008 60E70002
+71060010 41820008
+60E70800 71060040
+41820008 60E70400
+71060008 41820008
+60E71080 71060020
+41820008 60E70180
+71062000 41820008
+60E70080 71060200
+41820008 60E72000
+71060080 41820008
+60E71040 71060004
+4182000C 60E70050
+48000004 71060400
+41820008 60E70090
+71061000 41820008
+60E70200 7CE83B78
+4E800020 00000000
+*Left Stick variant toggles between movement and IR input (recommended!).
+*See README and CONTROL SCHEME for further instructions.
+
+$QOL - Classic Controller Support (Pointer on Right Stick) v1.1 [Vague Rant, crediar]
+C20FA624 00000003
+2C030002 40820008
+3863FFFF 3803FF04
+60000000 00000000
+C20FA68C 00000003
+2C040002 40820008
+3884FFFF 20640001
+60000000 00000000
+042CE284 48000010
+C2443F4C 00000003
+2C000001 4182000C
+C0230104 48000008
+C0230074 00000000
+C2443F68 00000003
+2C000001 4182000C
+C0230108 48000008
+C0230078 00000000
+C23BE9B8 0000001A
+88A40028 2C050002
+408200C0 88A3005E
+2C050002 4C820020
+9421FFF0 7C0802A6
+9001000C 90610008
+3CC0803A 60C6D5A0
+7CC803A6 4E800021
+2C030001 80610008
+48000005 7CA802A6
+C0450078 40820008
+EC4200B2 C065007C
+C0030020 C0230074
+FC211024 4800002D
+D0030020 C0030024
+C0230078 FC200850
+48000019 D0030024
+8001000C 7C0803A6
+38210010 4E800020
+FC0100FA C0250080
+FC000800 4180000C
+FC000890 48000014
+FC200850 FC000800
+41810008 FC000890
+4E800020 3FAAAAAB
+3C75C28F 3F800000
+9421FFC0 00000000
+C23BF9EC 00000026
+901F0068 811F0060
+48000061 80DF0000
+7D083378 911F0000
+7D0840F8 80DF0008
+7CC84038 911F0008
+811F0064 4800003D
+80DF0004 7D083378
+911F0004 71060040
+41820010 38C00002
+98DF005E 480000E0
+71060080 418200D8
+88DF005E 68C60002
+98DF005E 480000C8
+38E00000 71060800
+41820008 60E78000
+71060001 41820008
+60E70008 71064000
+41820008 60E70004
+71060002 41820008
+60E70001 71068000
+41820008 60E70002
+71060010 41820008
+60E70800 71060040
+41820008 60E70400
+71060008 41820008
+60E71080 71060020
+41820008 60E70180
+71062000 41820008
+60E70080 71060200
+41820008 60E72000
+71060080 41820008
+60E71040 71060004
+4182000C 60E70050
+48000004 71060400
+41820008 60E70090
+71061000 41820008
+60E70200 7CE83B78
+4E800020 00000000
+*Right Stick variant allows cursor movement while moving, but this is not a real advantage and may feel less convenient.
+*See README and CONTROL SCHEME for further instructions.
+
+[Gecko_RetroAchievements_Verified]
+$QOL - Classic Controller Support (Pointer on Left Stick, recommended) v1.1
+$QOL - Classic Controller Support (Pointer on Right Stick) v1.1 \ No newline at end of file
diff --git a/Data/Sys/GameSettings/RUUJ01r1.ini b/Data/Sys/GameSettings/RUUJ01r1.ini
new file mode 100644
index 0000000000..7bea0ebcab
--- /dev/null
+++ b/Data/Sys/GameSettings/RUUJ01r1.ini
@@ -0,0 +1,225 @@
+# RUUJ01 - Machi e Ikou yo: Doubutsu no Mori (NTSC-J, Rev 1)
+
+[Core]
+# Values set here will override the main Dolphin settings.
+
+[OnFrame]
+# Add memory patches to be applied every frame here.
+
+[ActionReplay]
+# Add action replay cheats here.
+
+[Gecko]
+# Add gecko cheats here.
+
+$!!! - README
+*It is required you configure your Wii Remote with a mapped Classic Controller extension!
+*Left Stick variant toggles between movement and IR input (recommended!).
+*Right Stick variant allows cursor movement while moving, but this is not a real advantage and may feel less convenient.
+*Pointer mode is activated intelligently, but can be forced on/off with the L button.
+*3C75C28F (float 0.015) sets cursor speed. You can adjust it between 3C23D70A (0.01) and 3CA3D70A (0.02).
+*To use the in-game keyboard, it is recommended to connect a real keyboard over using the pointer.
+*More information: https://gbatemp.net/threads/new-classic-controller-hacks.659837/post-10507857
+
+$!!! - CONTROL SCHEME
+*Remote & Nunchuk Classic Controller Game Function
+*--------------------------------------------------------------------
+*Remote Home Home Open/Close Home Menu
+*Remote D-Pad D-Pad Up: Change Camera
+* --- --- Left/Right: Change Tool
+* --- --- Down: Put Away Tool
+*Remote A A Confirm, Action
+*Remote B B Cancel, Run, Pick Up Item
+*Remote + Plus Open/Close Map
+* --- ZR Next Tab
+*Remote - X Open/Close Inventory
+* --- ZL Previous Tab
+*Remote 1 Minus Take Photo
+*Remote 2 Y Open/Close Photos
+*Nunchuk Stick Left Stick Movement
+*Nunchuk C N/A (Use A) Confirm, Action
+*Nunchuk Z R Cancel, Run, Pick Up Item
+*Remote Pointer L (Toggle) Move Cursor
+*
+*For the Pointer, use Left or Right Stick, depending on code used.
+*Home Menu works as normal, but remember to enable the pointer with L.
+
+$QOL - Classic Controller Support (Pointer on Left Stick, recommended) v1.1 [Vague Rant, crediar]
+C20FA8DC 00000003
+2C030002 40820008
+3863FFFF 3803FF04
+60000000 00000000
+C20FA944 00000003
+2C040002 40820008
+3884FFFF 20640001
+60000000 00000000
+042CE300 48000010
+C2443E80 00000003
+2C000001 4182000C
+C0230104 48000008
+C0230074 00000000
+C2443E9C 00000003
+2C000001 4182000C
+C0230108 48000008
+C0230078 00000000
+C23BE8F4 0000001C
+88A40028 2C050002
+408200CC 88A3005E
+2C050002 4C820020
+9421FFF0 7C0802A6
+9001000C 90610008
+3CC0803A 60C6D4DC
+7CC803A6 4E800021
+2C030001 80610008
+48000005 7CA802A6
+C0450084 40820008
+EC4200B2 C0650088
+C0030020 C023006C
+FC211024 48000039
+D0030020 C0030024
+C0230070 FC200850
+48000025 D0030024
+38000000 9003006C
+90030070 8001000C
+7C0803A6 38210010
+4E800020 FC0100FA
+C025008C FC000800
+4180000C FC000890
+48000014 FC200850
+FC000800 41810008
+FC000890 4E800020
+3FAAAAAB 3C75C28F
+3F800000 9421FFC0
+60000000 00000000
+C23BF928 00000026
+901F0068 811F0060
+48000061 80DF0000
+7D083378 911F0000
+7D0840F8 80DF0008
+7CC84038 911F0008
+811F0064 4800003D
+80DF0004 7D083378
+911F0004 71060040
+41820010 38C00002
+98DF005E 480000E0
+71060080 418200D8
+88DF005E 68C60002
+98DF005E 480000C8
+38E00000 71060800
+41820008 60E78000
+71060001 41820008
+60E70008 71064000
+41820008 60E70004
+71060002 41820008
+60E70001 71068000
+41820008 60E70002
+71060010 41820008
+60E70800 71060040
+41820008 60E70400
+71060008 41820008
+60E71080 71060020
+41820008 60E70180
+71062000 41820008
+60E70080 71060200
+41820008 60E72000
+71060080 41820008
+60E71040 71060004
+4182000C 60E70050
+48000004 71060400
+41820008 60E70090
+71061000 41820008
+60E70200 7CE83B78
+4E800020 00000000
+*Left Stick variant toggles between movement and IR input (recommended!).
+*See README and CONTROL SCHEME for further instructions.
+
+$QOL - Classic Controller Support (Pointer on Right Stick) v1.1 [Vague Rant, crediar]
+C20FA8DC 00000003
+2C030002 40820008
+3863FFFF 3803FF04
+60000000 00000000
+C20FA944 00000003
+2C040002 40820008
+3884FFFF 20640001
+60000000 00000000
+042CE300 48000010
+C2443E80 00000003
+2C000001 4182000C
+C0230104 48000008
+C0230074 00000000
+C2443E9C 00000003
+2C000001 4182000C
+C0230108 48000008
+C0230078 00000000
+C23BE8F4 0000001A
+88A40028 2C050002
+408200C0 88A3005E
+2C050002 4C820020
+9421FFF0 7C0802A6
+9001000C 90610008
+3CC0803A 60C6D4DC
+7CC803A6 4E800021
+2C030001 80610008
+48000005 7CA802A6
+C0450078 40820008
+EC4200B2 C065007C
+C0030020 C0230074
+FC211024 4800002D
+D0030020 C0030024
+C0230078 FC200850
+48000019 D0030024
+8001000C 7C0803A6
+38210010 4E800020
+FC0100FA C0250080
+FC000800 4180000C
+FC000890 48000014
+FC200850 FC000800
+41810008 FC000890
+4E800020 3FAAAAAB
+3C75C28F 3F800000
+9421FFC0 00000000
+C23BF928 00000026
+901F0068 811F0060
+48000061 80DF0000
+7D083378 911F0000
+7D0840F8 80DF0008
+7CC84038 911F0008
+811F0064 4800003D
+80DF0004 7D083378
+911F0004 71060040
+41820010 38C00002
+98DF005E 480000E0
+71060080 418200D8
+88DF005E 68C60002
+98DF005E 480000C8
+38E00000 71060800
+41820008 60E78000
+71060001 41820008
+60E70008 71064000
+41820008 60E70004
+71060002 41820008
+60E70001 71068000
+41820008 60E70002
+71060010 41820008
+60E70800 71060040
+41820008 60E70400
+71060008 41820008
+60E71080 71060020
+41820008 60E70180
+71062000 41820008
+60E70080 71060200
+41820008 60E72000
+71060080 41820008
+60E71040 71060004
+4182000C 60E70050
+48000004 71060400
+41820008 60E70090
+71061000 41820008
+60E70200 7CE83B78
+4E800020 00000000
+*Right Stick variant allows cursor movement while moving, but this is not a real advantage and may feel less convenient.
+*See README and CONTROL SCHEME for further instructions.
+
+[Gecko_RetroAchievements_Verified]
+$QOL - Classic Controller Support (Pointer on Left Stick, recommended) v1.1
+$QOL - Classic Controller Support (Pointer on Right Stick) v1.1 \ No newline at end of file
diff --git a/Data/Sys/GameSettings/RUUK01r1.ini b/Data/Sys/GameSettings/RUUK01r1.ini
new file mode 100644
index 0000000000..ad429263e4
--- /dev/null
+++ b/Data/Sys/GameSettings/RUUK01r1.ini
@@ -0,0 +1,225 @@
+# RUUK01 - Tauneuro Nolleogayo: Dongmurui Sup (NTSC-K, Rev 1)
+
+[Core]
+# Values set here will override the main Dolphin settings.
+
+[OnFrame]
+# Add memory patches to be applied every frame here.
+
+[ActionReplay]
+# Add action replay cheats here.
+
+[Gecko]
+# Add gecko cheats here.
+
+$!!! - README
+*It is required you configure your Wii Remote with a mapped Classic Controller extension!
+*Left Stick variant toggles between movement and IR input (recommended!).
+*Right Stick variant allows cursor movement while moving, but this is not a real advantage and may feel less convenient.
+*Pointer mode is activated intelligently, but can be forced on/off with the L button.
+*3C75C28F (float 0.015) sets cursor speed. You can adjust it between 3C23D70A (0.01) and 3CA3D70A (0.02).
+*To use the in-game keyboard, it is recommended to connect a real keyboard over using the pointer.
+*More information: https://gbatemp.net/threads/new-classic-controller-hacks.659837/post-10507857
+
+$!!! - CONTROL SCHEME
+*Remote & Nunchuk Classic Controller Game Function
+*--------------------------------------------------------------------
+*Remote Home Home Open/Close Home Menu
+*Remote D-Pad D-Pad Up: Change Camera
+* --- --- Left/Right: Change Tool
+* --- --- Down: Put Away Tool
+*Remote A A Confirm, Action
+*Remote B B Cancel, Run, Pick Up Item
+*Remote + Plus Open/Close Map
+* --- ZR Next Tab
+*Remote - X Open/Close Inventory
+* --- ZL Previous Tab
+*Remote 1 Minus Take Photo
+*Remote 2 Y Open/Close Photos
+*Nunchuk Stick Left Stick Movement
+*Nunchuk C N/A (Use A) Confirm, Action
+*Nunchuk Z R Cancel, Run, Pick Up Item
+*Remote Pointer L (Toggle) Move Cursor
+*
+*For the Pointer, use Left or Right Stick, depending on code used.
+*Home Menu works as normal, but remember to enable the pointer with L.
+
+$QOL - Classic Controller Support (Pointer on Left Stick, recommended) v1.1 [Vague Rant, crediar]
+C20FA6A4 00000003
+2C030002 40820008
+3863FFFF 3803FF04
+60000000 00000000
+C20FA70C 00000003
+2C040002 40820008
+3884FFFF 20640001
+60000000 00000000
+042CF840 48000010
+C244D8C4 00000003
+2C000001 4182000C
+C0230104 48000008
+C0230074 00000000
+C244D8E0 00000003
+2C000001 4182000C
+C0230108 48000008
+C0230078 00000000
+C23C72FC 0000001C
+88A40028 2C050002
+408200CC 88A3005E
+2C050002 4C820020
+9421FFF0 7C0802A6
+9001000C 90610008
+3CC0803B 60C65EE4
+7CC803A6 4E800021
+2C030001 80610008
+48000005 7CA802A6
+C0450084 40820008
+EC4200B2 C0650088
+C0030020 C023006C
+FC211024 48000039
+D0030020 C0030024
+C0230070 FC200850
+48000025 D0030024
+38000000 9003006C
+90030070 8001000C
+7C0803A6 38210010
+4E800020 FC0100FA
+C025008C FC000800
+4180000C FC000890
+48000014 FC200850
+FC000800 41810008
+FC000890 4E800020
+3FAAAAAB 3C75C28F
+3F800000 9421FFC0
+60000000 00000000
+C23C8330 00000026
+901F0068 811F0060
+48000061 80DF0000
+7D083378 911F0000
+7D0840F8 80DF0008
+7CC84038 911F0008
+811F0064 4800003D
+80DF0004 7D083378
+911F0004 71060040
+41820010 38C00002
+98DF005E 480000E0
+71060080 418200D8
+88DF005E 68C60002
+98DF005E 480000C8
+38E00000 71060800
+41820008 60E78000
+71060001 41820008
+60E70008 71064000
+41820008 60E70004
+71060002 41820008
+60E70001 71068000
+41820008 60E70002
+71060010 41820008
+60E70800 71060040
+41820008 60E70400
+71060008 41820008
+60E71080 71060020
+41820008 60E70180
+71062000 41820008
+60E70080 71060200
+41820008 60E72000
+71060080 41820008
+60E71040 71060004
+4182000C 60E70050
+48000004 71060400
+41820008 60E70090
+71061000 41820008
+60E70200 7CE83B78
+4E800020 00000000
+*Left Stick variant toggles between movement and IR input (recommended!).
+*See README and CONTROL SCHEME for further instructions.
+
+$QOL - Classic Controller Support (Pointer on Right Stick) v1.1 [Vague Rant, crediar]
+C20FA6A4 00000003
+2C030002 40820008
+3863FFFF 3803FF04
+60000000 00000000
+C20FA70C 00000003
+2C040002 40820008
+3884FFFF 20640001
+60000000 00000000
+042CF840 48000010
+C244D8C4 00000003
+2C000001 4182000C
+C0230104 48000008
+C0230074 00000000
+C244D8E0 00000003
+2C000001 4182000C
+C0230108 48000008
+C0230078 00000000
+C23C72FC 0000001A
+88A40028 2C050002
+408200C0 88A3005E
+2C050002 4C820020
+9421FFF0 7C0802A6
+9001000C 90610008
+3CC0803B 60C65EE4
+7CC803A6 4E800021
+2C030001 80610008
+48000005 7CA802A6
+C0450078 40820008
+EC4200B2 C065007C
+C0030020 C0230074
+FC211024 4800002D
+D0030020 C0030024
+C0230078 FC200850
+48000019 D0030024
+8001000C 7C0803A6
+38210010 4E800020
+FC0100FA C0250080
+FC000800 4180000C
+FC000890 48000014
+FC200850 FC000800
+41810008 FC000890
+4E800020 3FAAAAAB
+3C75C28F 3F800000
+9421FFC0 00000000
+C23C8330 00000026
+901F0068 811F0060
+48000061 80DF0000
+7D083378 911F0000
+7D0840F8 80DF0008
+7CC84038 911F0008
+811F0064 4800003D
+80DF0004 7D083378
+911F0004 71060040
+41820010 38C00002
+98DF005E 480000E0
+71060080 418200D8
+88DF005E 68C60002
+98DF005E 480000C8
+38E00000 71060800
+41820008 60E78000
+71060001 41820008
+60E70008 71064000
+41820008 60E70004
+71060002 41820008
+60E70001 71068000
+41820008 60E70002
+71060010 41820008
+60E70800 71060040
+41820008 60E70400
+71060008 41820008
+60E71080 71060020
+41820008 60E70180
+71062000 41820008
+60E70080 71060200
+41820008 60E72000
+71060080 41820008
+60E71040 71060004
+4182000C 60E70050
+48000004 71060400
+41820008 60E70090
+71061000 41820008
+60E70200 7CE83B78
+4E800020 00000000
+*Right Stick variant allows cursor movement while moving, but this is not a real advantage and may feel less convenient.
+*See README and CONTROL SCHEME for further instructions.
+
+[Gecko_RetroAchievements_Verified]
+$QOL - Classic Controller Support (Pointer on Left Stick, recommended) v1.1
+$QOL - Classic Controller Support (Pointer on Right Stick) v1.1 \ No newline at end of file
diff --git a/Data/Sys/GameSettings/RUUP01r0.ini b/Data/Sys/GameSettings/RUUP01r0.ini
new file mode 100644
index 0000000000..44a5df6e3e
--- /dev/null
+++ b/Data/Sys/GameSettings/RUUP01r0.ini
@@ -0,0 +1,233 @@
+# RUUP01 - Animal Crossing: Let's Go to the City (PAL, Rev 0)
+
+[Core]
+# Values set here will override the main Dolphin settings.
+
+[OnFrame]
+# Add memory patches to be applied every frame here.
+
+[ActionReplay]
+# Add action replay cheats here.
+
+[Gecko]
+# Add gecko cheats here.
+
+$!!! - README
+*It is required you configure your Wii Remote with a mapped Classic Controller extension!
+*Left Stick variant toggles between movement and IR input (recommended!).
+*Right Stick variant allows cursor movement while moving, but this is not a real advantage and may feel less convenient.
+*Pointer mode is activated intelligently, but can be forced on/off with the L button.
+*3C75C28F (float 0.015) sets cursor speed. You can adjust it between 3C23D70A (0.01) and 3CA3D70A (0.02).
+*To use the in-game keyboard, it is recommended to connect a real keyboard over using the pointer.
+*More information: https://gbatemp.net/threads/new-classic-controller-hacks.659837/post-10507857
+
+$!!! - CONTROL SCHEME
+*Remote & Nunchuk Classic Controller Game Function
+*--------------------------------------------------------------------
+*Remote Home Home Open/Close Home Menu
+*Remote D-Pad D-Pad Up: Change Camera
+* --- --- Left/Right: Change Tool
+* --- --- Down: Put Away Tool
+*Remote A A Confirm, Action
+*Remote B B Cancel, Run, Pick Up Item
+*Remote + Plus Open/Close Map
+* --- ZR Next Tab
+*Remote - X Open/Close Inventory
+* --- ZL Previous Tab
+*Remote 1 Minus Take Photo
+*Remote 2 Y Open/Close Photos
+*Nunchuk Stick Left Stick Movement
+*Nunchuk C N/A (Use A) Confirm, Action
+*Nunchuk Z R Cancel, Run, Pick Up Item
+*Remote Pointer L (Toggle) Move Cursor
+*
+*For the Pointer, use Left or Right Stick, depending on code used.
+*Home Menu works as normal, but remember to enable the pointer with L.
+
+$QOL - Classic Controller Support (Pointer on Left Stick, recommended) v1.1 [Vague Rant, crediar]
+C20FA784 00000003
+2C030002 40820008
+3863FFFF 3803FF04
+60000000 00000000
+C20FA7EC 00000003
+2C040002 40820008
+3884FFFF 20640001
+60000000 00000000
+042CDFB0 48000010
+C2443CC8 00000003
+2C000001 4182000C
+C0230104 48000008
+C0230074 00000000
+C2443CE4 00000003
+2C000001 4182000C
+C0230108 48000008
+C0230078 00000000
+C23BE5A0 0000001C
+88A40028 2C050002
+408200CC 88A3005E
+2C050002 4C820020
+9421FFF0 7C0802A6
+9001000C 90610008
+3CC0803A 60C6D188
+7CC803A6 4E800021
+2C030001 80610008
+48000005 7CA802A6
+C0450084 40820008
+EC4200B2 C0650088
+C0030020 C023006C
+FC211024 48000039
+D0030020 C0030024
+C0230070 FC200850
+48000025 D0030024
+38000000 9003006C
+90030070 8001000C
+7C0803A6 38210010
+4E800020 FC0100FA
+C025008C FC000800
+4180000C FC000890
+48000014 FC200850
+FC000800 41810008
+FC000890 4E800020
+3FAAAAAB 3C75C28F
+3F800000 9421FFC0
+60000000 00000000
+C23BF5D4 00000026
+901F0068 811F0060
+48000061 80DF0000
+7D083378 911F0000
+7D0840F8 80DF0008
+7CC84038 911F0008
+811F0064 4800003D
+80DF0004 7D083378
+911F0004 71060040
+41820010 38C00002
+98DF005E 480000E0
+71060080 418200D8
+88DF005E 68C60002
+98DF005E 480000C8
+38E00000 71060800
+41820008 60E78000
+71060001 41820008
+60E70008 71064000
+41820008 60E70004
+71060002 41820008
+60E70001 71068000
+41820008 60E70002
+71060010 41820008
+60E70800 71060040
+41820008 60E70400
+71060008 41820008
+60E71080 71060020
+41820008 60E70180
+71062000 41820008
+60E70080 71060200
+41820008 60E72000
+71060080 41820008
+60E71040 71060004
+4182000C 60E70050
+48000004 71060400
+41820008 60E70090
+71061000 41820008
+60E70200 7CE83B78
+4E800020 00000000
+*Left Stick variant toggles between movement and IR input (recommended!).
+*See README and CONTROL SCHEME for further instructions.
+
+$QOL - Classic Controller Support (Pointer on Right Stick) v1.1 [Vague Rant, crediar]
+C20FA784 00000003
+2C030002 40820008
+3863FFFF 3803FF04
+60000000 00000000
+C20FA7EC 00000003
+2C040002 40820008
+3884FFFF 20640001
+60000000 00000000
+042CDFB0 48000010
+C2443CC8 00000003
+2C000001 4182000C
+C0230104 48000008
+C0230074 00000000
+C2443CE4 00000003
+2C000001 4182000C
+C0230108 48000008
+C0230078 00000000
+C23BE5A0 0000001A
+88A40028 2C050002
+408200C0 88A3005E
+2C050002 4C820020
+9421FFF0 7C0802A6
+9001000C 90610008
+3CC0803A 60C6D188
+7CC803A6 4E800021
+2C030001 80610008
+48000005 7CA802A6
+C0450078 40820008
+EC4200B2 C065007C
+C0030020 C0230074
+FC211024 4800002D
+D0030020 C0030024
+C0230078 FC200850
+48000019 D0030024
+8001000C 7C0803A6
+38210010 4E800020
+FC0100FA C0250080
+FC000800 4180000C
+FC000890 48000014
+FC200850 FC000800
+41810008 FC000890
+4E800020 3FAAAAAB
+3C75C28F 3F800000
+9421FFC0 00000000
+C23BF5D4 00000026
+901F0068 811F0060
+48000061 80DF0000
+7D083378 911F0000
+7D0840F8 80DF0008
+7CC84038 911F0008
+811F0064 4800003D
+80DF0004 7D083378
+911F0004 71060040
+41820010 38C00002
+98DF005E 480000E0
+71060080 418200D8
+88DF005E 68C60002
+98DF005E 480000C8
+38E00000 71060800
+41820008 60E78000
+71060001 41820008
+60E70008 71064000
+41820008 60E70004
+71060002 41820008
+60E70001 71068000
+41820008 60E70002
+71060010 41820008
+60E70800 71060040
+41820008 60E70400
+71060008 41820008
+60E71080 71060020
+41820008 60E70180
+71062000 41820008
+60E70080 71060200
+41820008 60E72000
+71060080 41820008
+60E71040 71060004
+4182000C 60E70050
+48000004 71060400
+41820008 60E70090
+71061000 41820008
+60E70200 7CE83B78
+4E800020 00000000
+*Right Stick variant allows cursor movement while moving, but this is not a real advantage and may feel less convenient.
+*See README and CONTROL SCHEME for further instructions.
+
+$QOL - Disable Grass Deterioration [Seth@WiiPlaza, Fox_McCloud45]
+042B613C 60000000
+*Prevents grass from deteriorating when walked on.
+*Hides deteriorating grass patches, e.g., around houses.
+*Does not modify pre-existing save file data; grass will still be deteriorated when disabled.
+*While active, save file data remains static; grass will neither heal nor deteriorate in save data.
+
+[Gecko_RetroAchievements_Verified]
+$QOL - Classic Controller Support (Pointer on Left Stick, recommended) v1.1
+$QOL - Classic Controller Support (Pointer on Right Stick) v1.1
+$QOL - Disable Grass Deterioration \ No newline at end of file
diff --git a/Data/Sys/GameSettings/RUUP01r1.ini b/Data/Sys/GameSettings/RUUP01r1.ini
new file mode 100644
index 0000000000..a39e65f953
--- /dev/null
+++ b/Data/Sys/GameSettings/RUUP01r1.ini
@@ -0,0 +1,233 @@
+# RUUP01 - Animal Crossing: Let's Go to the City (PAL, Rev 1)
+
+[Core]
+# Values set here will override the main Dolphin settings.
+
+[OnFrame]
+# Add memory patches to be applied every frame here.
+
+[ActionReplay]
+# Add action replay cheats here.
+
+[Gecko]
+# Add gecko cheats here.
+
+$!!! - README
+*It is required you configure your Wii Remote with a mapped Classic Controller extension!
+*Left Stick variant toggles between movement and IR input (recommended!).
+*Right Stick variant allows cursor movement while moving, but this is not a real advantage and may feel less convenient.
+*Pointer mode is activated intelligently, but can be forced on/off with the L button.
+*3C75C28F (float 0.015) sets cursor speed. You can adjust it between 3C23D70A (0.01) and 3CA3D70A (0.02).
+*To use the in-game keyboard, it is recommended to connect a real keyboard over using the pointer.
+*More information: https://gbatemp.net/threads/new-classic-controller-hacks.659837/post-10507857
+
+$!!! - CONTROL SCHEME
+*Remote & Nunchuk Classic Controller Game Function
+*--------------------------------------------------------------------
+*Remote Home Home Open/Close Home Menu
+*Remote D-Pad D-Pad Up: Change Camera
+* --- --- Left/Right: Change Tool
+* --- --- Down: Put Away Tool
+*Remote A A Confirm, Action
+*Remote B B Cancel, Run, Pick Up Item
+*Remote + Plus Open/Close Map
+* --- ZR Next Tab
+*Remote - X Open/Close Inventory
+* --- ZL Previous Tab
+*Remote 1 Minus Take Photo
+*Remote 2 Y Open/Close Photos
+*Nunchuk Stick Left Stick Movement
+*Nunchuk C N/A (Use A) Confirm, Action
+*Nunchuk Z R Cancel, Run, Pick Up Item
+*Remote Pointer L (Toggle) Move Cursor
+*
+*For the Pointer, use Left or Right Stick, depending on code used.
+*Home Menu works as normal, but remember to enable the pointer with L.
+
+$QOL - Classic Controller Support (Pointer on Left Stick, recommended) v1.1 [Vague Rant, crediar]
+C20FA784 00000003
+2C030002 40820008
+3863FFFF 3803FF04
+60000000 00000000
+C20FA7EC 00000003
+2C040002 40820008
+3884FFFF 20640001
+60000000 00000000
+042CE0D4 48000010
+C2443F30 00000003
+2C000001 4182000C
+C0230104 48000008
+C0230074 00000000
+C2443F4C 00000003
+2C000001 4182000C
+C0230108 48000008
+C0230078 00000000
+C23BE808 0000001C
+88A40028 2C050002
+408200CC 88A3005E
+2C050002 4C820020
+9421FFF0 7C0802A6
+9001000C 90610008
+3CC0803A 60C6D3F0
+7CC803A6 4E800021
+2C030001 80610008
+48000005 7CA802A6
+C0450084 40820008
+EC4200B2 C0650088
+C0030020 C023006C
+FC211024 48000039
+D0030020 C0030024
+C0230070 FC200850
+48000025 D0030024
+38000000 9003006C
+90030070 8001000C
+7C0803A6 38210010
+4E800020 FC0100FA
+C025008C FC000800
+4180000C FC000890
+48000014 FC200850
+FC000800 41810008
+FC000890 4E800020
+3FAAAAAB 3C75C28F
+3F800000 9421FFC0
+60000000 00000000
+C23BF83C 00000026
+901F0068 811F0060
+48000061 80DF0000
+7D083378 911F0000
+7D0840F8 80DF0008
+7CC84038 911F0008
+811F0064 4800003D
+80DF0004 7D083378
+911F0004 71060040
+41820010 38C00002
+98DF005E 480000E0
+71060080 418200D8
+88DF005E 68C60002
+98DF005E 480000C8
+38E00000 71060800
+41820008 60E78000
+71060001 41820008
+60E70008 71064000
+41820008 60E70004
+71060002 41820008
+60E70001 71068000
+41820008 60E70002
+71060010 41820008
+60E70800 71060040
+41820008 60E70400
+71060008 41820008
+60E71080 71060020
+41820008 60E70180
+71062000 41820008
+60E70080 71060200
+41820008 60E72000
+71060080 41820008
+60E71040 71060004
+4182000C 60E70050
+48000004 71060400
+41820008 60E70090
+71061000 41820008
+60E70200 7CE83B78
+4E800020 00000000
+*Left Stick variant toggles between movement and IR input (recommended!).
+*See README and CONTROL SCHEME for further instructions.
+
+$QOL - Classic Controller Support (Pointer on Right Stick) v1.1 [Vague Rant, crediar]
+C20FA784 00000003
+2C030002 40820008
+3863FFFF 3803FF04
+60000000 00000000
+C20FA7EC 00000003
+2C040002 40820008
+3884FFFF 20640001
+60000000 00000000
+042CE0D4 48000010
+C2443F30 00000003
+2C000001 4182000C
+C0230104 48000008
+C0230074 00000000
+C2443F4C 00000003
+2C000001 4182000C
+C0230108 48000008
+C0230078 00000000
+C23BE808 0000001A
+88A40028 2C050002
+408200C0 88A3005E
+2C050002 4C820020
+9421FFF0 7C0802A6
+9001000C 90610008
+3CC0803A 60C6D3F0
+7CC803A6 4E800021
+2C030001 80610008
+48000005 7CA802A6
+C0450078 40820008
+EC4200B2 C065007C
+C0030020 C0230074
+FC211024 4800002D
+D0030020 C0030024
+C0230078 FC200850
+48000019 D0030024
+8001000C 7C0803A6
+38210010 4E800020
+FC0100FA C0250080
+FC000800 4180000C
+FC000890 48000014
+FC200850 FC000800
+41810008 FC000890
+4E800020 3FAAAAAB
+3C75C28F 3F800000
+9421FFC0 00000000
+C23BF83C 00000026
+901F0068 811F0060
+48000061 80DF0000
+7D083378 911F0000
+7D0840F8 80DF0008
+7CC84038 911F0008
+811F0064 4800003D
+80DF0004 7D083378
+911F0004 71060040
+41820010 38C00002
+98DF005E 480000E0
+71060080 418200D8
+88DF005E 68C60002
+98DF005E 480000C8
+38E00000 71060800
+41820008 60E78000
+71060001 41820008
+60E70008 71064000
+41820008 60E70004
+71060002 41820008
+60E70001 71068000
+41820008 60E70002
+71060010 41820008
+60E70800 71060040
+41820008 60E70400
+71060008 41820008
+60E71080 71060020
+41820008 60E70180
+71062000 41820008
+60E70080 71060200
+41820008 60E72000
+71060080 41820008
+60E71040 71060004
+4182000C 60E70050
+48000004 71060400
+41820008 60E70090
+71061000 41820008
+60E70200 7CE83B78
+4E800020 00000000
+*Right Stick variant allows cursor movement while moving, but this is not a real advantage and may feel less convenient.
+*See README and CONTROL SCHEME for further instructions.
+
+$QOL - Disable Grass Deterioration [Seth@WiiPlaza, Fox_McCloud45]
+042B6260 60000000
+*Prevents grass from deteriorating when walked on.
+*Hides deteriorating grass patches, e.g., around houses.
+*Does not modify pre-existing save file data; grass will still be deteriorated when disabled.
+*While active, save file data remains static; grass will neither heal nor deteriorate in save data.
+
+[Gecko_RetroAchievements_Verified]
+$QOL - Classic Controller Support (Pointer on Left Stick, recommended) v1.1
+$QOL - Classic Controller Support (Pointer on Right Stick) v1.1
+$QOL - Disable Grass Deterioration \ No newline at end of file
diff --git a/Data/Sys/GameSettings/RZD.ini b/Data/Sys/GameSettings/RZD.ini
index 6a46bbdba0..424edfcfc1 100644
--- a/Data/Sys/GameSettings/RZD.ini
+++ b/Data/Sys/GameSettings/RZD.ini
@@ -1,4 +1,5 @@
# RZDE01, RZDJ01, RZDK01, RZDP01 - The Legend of Zelda: Twilight Princess [Wii]
+
[Core]
# Values set here will override the main Dolphin settings.
diff --git a/Data/Sys/GameSettings/SMB.ini b/Data/Sys/GameSettings/SMB.ini
index ede5339f82..0a69bc6ee9 100644
--- a/Data/Sys/GameSettings/SMB.ini
+++ b/Data/Sys/GameSettings/SMB.ini
@@ -1,4 +1,4 @@
-# SMBE8P, SMBP8P - SUPER MONKEY BALL STEP AND ROLL
+# SMBE8P, SMBP8P - Super Monkey Ball: Step and Roll
[Core]
# Values set here will override the main Dolphin settings.
diff --git a/Externals/licenses.md b/Externals/licenses.md
index 727a41e2a1..7aed5313ac 100644
--- a/Externals/licenses.md
+++ b/Externals/licenses.md
@@ -76,6 +76,8 @@ Dolphin includes or links code of the following third-party software projects:
[MIT](https://github.com/microsoft/wil/blob/master/LICENSE)
- [xxHash](https://github.com/Cyan4973/xxHash):
[BSD 2-Clause](https://github.com/Cyan4973/xxHash/blob/master/LICENSE)
+- [YACardEmu](https://github.com/GXTX/YACardEmu)
+ [GPLv2+](https://github.com/GXTX/YACardEmu/blob/master/license.txt)
- [zlib-ng](https://github.com/zlib-ng/zlib-ng):
[zlib license](https://github.com/zlib-ng/zlib-ng/blob/develop/LICENSE.md)
- [Zstandard](https://facebook.github.io/zstd/):
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.kt
index 360800d656..71b37ff513 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.kt
@@ -95,11 +95,6 @@ object ControllerInterface {
deviceQualifier: String, axisNames: Array<String>, suspended: Boolean
)
- /**
- * Rescans for input devices.
- */
- external fun refreshDevices()
-
external fun getAllDeviceStrings(): Array<String>
external fun getDevice(deviceString: String): CoreDevice?
@@ -193,13 +188,10 @@ object ControllerInterface {
}
private class InputDeviceListener : InputManager.InputDeviceListener {
- // Simple implementation for now. We could do something fancier if we wanted to.
- override fun onInputDeviceAdded(deviceId: Int) = refreshDevices()
+ override external fun onInputDeviceAdded(deviceId: Int)
- // Simple implementation for now. We could do something fancier if we wanted to.
- override fun onInputDeviceRemoved(deviceId: Int) = refreshDevices()
+ override external fun onInputDeviceRemoved(deviceId: Int)
- // Simple implementation for now. We could do something fancier if we wanted to.
- override fun onInputDeviceChanged(deviceId: Int) = refreshDevices()
+ override external fun onInputDeviceChanged(deviceId: Int)
}
}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt
index 29640aa92f..c4f1107bcd 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/StringSetting.kt
@@ -69,6 +69,12 @@ enum class StringSetting(
""
),
MAIN_WFS_PATH(Settings.FILE_DOLPHIN, Settings.SECTION_INI_GENERAL, "WFSPath", ""),
+ MAIN_TRIFORCE_IP_REDIRECTIONS(
+ Settings.FILE_DOLPHIN,
+ Settings.SECTION_INI_CORE,
+ "TriforceIPRedirections",
+ "0.0.0.0/0=127.0.0.1"
+ ),
GFX_ENHANCE_POST_SHADER(
Settings.FILE_GFX,
Settings.SECTION_GFX_ENHANCEMENTS,
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt
index 7cf0d0ecad..1f68bbbd1a 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.kt
@@ -1169,7 +1169,7 @@ class SettingsFragmentPresenter(
R.string.overclock_title,
R.string.overclock_title_description,
0f,
- 400f,
+ 500f,
"%",
1f,
false
@@ -1276,7 +1276,17 @@ class SettingsFragmentPresenter(
}
private fun addSerialPortSubSettings(sl: ArrayList<SettingsItem>, serialPort1Type: Int) {
- if (serialPort1Type == 10) {
+ if (serialPort1Type == 6) {
+ // Triforce Baseboard
+ sl.add(
+ InputStringSetting(
+ context,
+ StringSetting.MAIN_TRIFORCE_IP_REDIRECTIONS,
+ R.string.triforce_ip_redirections,
+ 0
+ )
+ )
+ } else if (serialPort1Type == 10) {
// Broadband Adapter (XLink Kai)
sl.add(HyperLinkHeaderSetting(context, R.string.xlink_kai_guide_header, 0))
sl.add(
@@ -2352,7 +2362,7 @@ class SettingsFragmentPresenter(
private fun addGcPadSubSettings(sl: ArrayList<SettingsItem>, gcPadNumber: Int, gcPadType: Int) {
when (gcPadType) {
- 6, 8, 9, 10 -> {
+ 6, 8, 9, 10, 11 -> {
// Emulated
val gcPad = EmulatedController.getGcPad(gcPadNumber)
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlay.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlay.kt
index 7964cc1ebc..3d991fed1e 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlay.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlay.kt
@@ -2285,6 +2285,7 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex
const val OVERLAY_NONE = 5
private const val DISABLED_GAMECUBE_CONTROLLER = 0
private const val EMULATED_GAMECUBE_CONTROLLER = 6
+ private const val EMULATED_AM_BASEBOARD = 11
private const val GAMECUBE_ADAPTER = 12
// Buttons that have special positions in Wiimote only
@@ -2337,8 +2338,11 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex
if (controllerIndex in 0 until 4) {
// GameCube controller
- if (getSettingForSIDevice(controllerIndex).int == 6)
- return OVERLAY_GAMECUBE
+ when (getSettingForSIDevice(controllerIndex).int) {
+ EMULATED_GAMECUBE_CONTROLLER, EMULATED_AM_BASEBOARD -> {
+ return OVERLAY_GAMECUBE
+ }
+ }
} else if (controllerIndex in 4 until 8) {
// Wii Remote
val wiimoteIndex = controllerIndex - 4
diff --git a/Source/Android/app/src/main/res/values/arrays.xml b/Source/Android/app/src/main/res/values/arrays.xml
index 8892f7f218..d919edb1d2 100644
--- a/Source/Android/app/src/main/res/values/arrays.xml
+++ b/Source/Android/app/src/main/res/values/arrays.xml
@@ -105,6 +105,7 @@
<item>@string/broadband_adapter_hle</item>
<item>@string/broadband_adapter_tapserver</item>
<item>@string/modem_adapter_tapserver</item>
+ <item>@string/sp1_am_baseboard</item>
</string-array>
<integer-array name="serialPort1DeviceValues">
<item>255</item>
@@ -113,6 +114,7 @@
<item>12</item>
<item>11</item>
<item>13</item>
+ <item>6</item>
</integer-array>
<!-- Wii System Languages -->
@@ -375,6 +377,7 @@
<item>@string/gcpad_steering_wheel</item>
<item>@string/gcpad_dance_mat</item>
<item>@string/gcpad_taru_konga</item>
+ <item>@string/gcpad_am_baseboard</item>
<item>@string/gcpad_gc_adapter</item>
</string-array>
<integer-array name="gcpadTypeValues">
@@ -384,6 +387,7 @@
<item>8</item>
<item>9</item>
<item>10</item>
+ <item>11</item>
<item>12</item>
</integer-array>
diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml
index b1630def81..855cb56ab8 100644
--- a/Source/Android/app/src/main/res/values/strings.xml
+++ b/Source/Android/app/src/main/res/values/strings.xml
@@ -142,6 +142,7 @@
<string name="modem_tapserver_destination_description">Enter the socket path or netloc (address:port) of the tapserver instance</string>
<string name="bba_builtin_dns">DNS Server</string>
<string name="bba_builtin_dns_description">Use 8.8.8.8 for normal DNS, else enter your custom one</string>
+ <string name="triforce_ip_redirections">IP Redirections</string>
<!-- Interface Preference Fragment -->
<string name="interface_submenu">Interface</string>
@@ -707,6 +708,7 @@ It can efficiently compress both junk data and encrypted Wii data.
<string name="broadband_adapter_hle">Broadband Adapter (HLE)</string>
<string name="broadband_adapter_tapserver">Broadband Adapter (tapserver)</string>
<string name="modem_adapter_tapserver">Modem Adapter (tapserver)</string>
+ <string name="sp1_am_baseboard">Triforce Baseboard</string>
<!-- Sound Mode -->
<string name="sound_mode_mono">Mono</string>
@@ -811,6 +813,7 @@ It can efficiently compress both junk data and encrypted Wii data.
<string name="gcpad_steering_wheel">Steering Wheel</string>
<string name="gcpad_dance_mat">Dance Mat</string>
<string name="gcpad_taru_konga">DK Bongos</string>
+ <string name="gcpad_am_baseboard">Triforce Baseboard</string>
<string name="gcpad_gc_adapter">GameCube Adapter</string>
<!-- Wiimote Types -->
diff --git a/Source/Android/jni/Config/NativeConfig.cpp b/Source/Android/jni/Config/NativeConfig.cpp
index 8478174dae..c524c0ea9e 100644
--- a/Source/Android/jni/Config/NativeConfig.cpp
+++ b/Source/Android/jni/Config/NativeConfig.cpp
@@ -11,7 +11,6 @@
#include "Core/ConfigLoaders/GameConfigLoader.h"
#include "Core/ConfigLoaders/IsSettingSaveable.h"
#include "jni/AndroidCommon/AndroidCommon.h"
-#include "jni/Host.h"
constexpr jint LAYER_BASE_OR_CURRENT = 0;
constexpr jint LAYER_BASE = 1;
@@ -143,10 +142,6 @@ Java_org_dolphinemu_dolphinemu_features_settings_model_NativeConfig_unloadGameIn
JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_features_settings_model_NativeConfig_save(
JNIEnv*, jclass, jint layer)
{
- // HostThreadLock is used to ensure we don't try to save to SYSCONF at the same time as
- // emulation shutdown does
- HostThreadLock guard;
-
return GetLayer(layer, {})->Save();
}
diff --git a/Source/Core/Common/BitUtils.h b/Source/Core/Common/BitUtils.h
index 19eea49486..458fff63a3 100644
--- a/Source/Core/Common/BitUtils.h
+++ b/Source/Core/Common/BitUtils.h
@@ -189,7 +189,7 @@ template <typename ValueType, typename From>
}
template <typename T>
-void SetBit(T& value, size_t bit_number, bool bit_value)
+constexpr void SetBit(T& value, size_t bit_number, bool bit_value)
{
static_assert(std::is_unsigned<T>(), "SetBit is only sane on unsigned types.");
@@ -200,7 +200,7 @@ void SetBit(T& value, size_t bit_number, bool bit_value)
}
template <size_t bit_number, typename T>
-void SetBit(T& value, bool bit_value)
+constexpr void SetBit(T& value, bool bit_value)
{
SetBit(value, bit_number, bit_value);
}
diff --git a/Source/Core/Common/ChunkFile.h b/Source/Core/Common/ChunkFile.h
index 7e48e6e913..f44f08d279 100644
--- a/Source/Core/Common/ChunkFile.h
+++ b/Source/Core/Common/ChunkFile.h
@@ -264,9 +264,9 @@ public:
}
template <typename T>
+ requires(std::is_trivially_copyable_v<T> && !std::is_pointer_v<T>)
void Do(T& x)
{
- static_assert(std::is_trivially_copyable_v<T>, "Only sane for trivially copyable types");
// Note:
// Usually we can just use x = **ptr, etc. However, this doesn't work
// for unions containing BitFields (long story, stupid language rules)
diff --git a/Source/Core/Common/CommonPaths.h b/Source/Core/Common/CommonPaths.h
index d0efa66006..474890c4d8 100644
--- a/Source/Core/Common/CommonPaths.h
+++ b/Source/Core/Common/CommonPaths.h
@@ -41,11 +41,13 @@
#define USA_DIR "USA"
#define JAP_DIR "JAP"
#define JPN_DIR "JPN"
+#define DEV_DIR "DEV"
// Subdirs in the User dir returned by GetUserPath(D_USER_IDX)
#define GC_USER_DIR "GC"
#define GBA_USER_DIR "GBA"
#define WII_USER_DIR "Wii"
+#define TRI_USER_DIR "Triforce"
#define CONFIG_DIR "Config"
#define GAMESETTINGS_DIR "GameSettings"
#define MAPS_DIR "Maps"
@@ -155,6 +157,7 @@
// Subdirs in Sys
#define GC_SYS_DIR "GC"
#define WII_SYS_DIR "Wii"
+#define TRI_SYS_DIR "Triforce"
// Subdirs in Config
#define GRAPHICSMOD_CONFIG_DIR "GraphicMods"
diff --git a/Source/Core/Common/FileUtil.cpp b/Source/Core/Common/FileUtil.cpp
index 05be18b610..1dbbb4ac41 100644
--- a/Source/Core/Common/FileUtil.cpp
+++ b/Source/Core/Common/FileUtil.cpp
@@ -838,6 +838,7 @@ static void RebuildUserDirectories(unsigned int dir_index)
case D_USER_IDX:
s_user_paths[D_GCUSER_IDX] = s_user_paths[D_USER_IDX] + GC_USER_DIR DIR_SEP;
s_user_paths[D_WIIROOT_IDX] = s_user_paths[D_USER_IDX] + WII_USER_DIR DIR_SEP;
+ s_user_paths[D_TRIUSER_IDX] = s_user_paths[D_USER_IDX] + TRI_USER_DIR DIR_SEP;
s_user_paths[D_CONFIG_IDX] = s_user_paths[D_USER_IDX] + CONFIG_DIR DIR_SEP;
s_user_paths[D_GAMESETTINGS_IDX] = s_user_paths[D_USER_IDX] + GAMESETTINGS_DIR DIR_SEP;
s_user_paths[D_MAPS_IDX] = s_user_paths[D_USER_IDX] + MAPS_DIR DIR_SEP;
diff --git a/Source/Core/Common/FileUtil.h b/Source/Core/Common/FileUtil.h
index 49f6847768..cfe6eddffb 100644
--- a/Source/Core/Common/FileUtil.h
+++ b/Source/Core/Common/FileUtil.h
@@ -26,7 +26,8 @@ enum
{
D_USER_IDX,
D_GCUSER_IDX,
- D_WIIROOT_IDX, // always points to User/Wii or global user-configured directory
+ D_WIIROOT_IDX, // always points to User/Wii or global user-configured directory
+ D_TRIUSER_IDX,
D_SESSION_WIIROOT_IDX, // may point to minimal temporary directory for determinism
D_CONFIG_IDX, // global settings
D_GAMESETTINGS_IDX, // user-specified settings which override both the global and the default
diff --git a/Source/Core/Common/Logging/Log.h b/Source/Core/Common/Logging/Log.h
index 9e55f779cd..64d353af00 100644
--- a/Source/Core/Common/Logging/Log.h
+++ b/Source/Core/Common/Logging/Log.h
@@ -27,6 +27,8 @@ enum class LogType : int
DSP_MAIL,
DSPINTERFACE,
DVDINTERFACE,
+ AMMEDIABOARD,
+ AMMEDIABOARD_NET,
DYNA_REC,
EXPANSIONINTERFACE,
FILEMON,
@@ -58,6 +60,9 @@ enum class LogType : int
PROCESSORINTERFACE,
POWERPC,
SERIALINTERFACE,
+ SERIALINTERFACE_AMBB,
+ SERIALINTERFACE_CARD,
+ SERIALINTERFACE_JVSIO,
SP1,
SYMBOLS,
VIDEO,
diff --git a/Source/Core/Common/Logging/LogManager.cpp b/Source/Core/Common/Logging/LogManager.cpp
index 0755dc92ab..6c15608420 100644
--- a/Source/Core/Common/Logging/LogManager.cpp
+++ b/Source/Core/Common/Logging/LogManager.cpp
@@ -111,6 +111,8 @@ LogManager::LogManager()
m_log[LogType::DSP_MAIL] = {"DSPMails", "DSP Mails"};
m_log[LogType::DSPINTERFACE] = {"DSP", "DSP Interface"};
m_log[LogType::DVDINTERFACE] = {"DVD", "DVD Interface"};
+ m_log[LogType::AMMEDIABOARD] = {"AMMB", "AMMB Interface"};
+ m_log[LogType::AMMEDIABOARD_NET] = {"AMMB_NET", "AMMB - Network"};
m_log[LogType::DYNA_REC] = {"JIT", "JIT Dynamic Recompiler"};
m_log[LogType::EXPANSIONINTERFACE] = {"EXI", "Expansion Interface"};
m_log[LogType::FILEMON] = {"FileMon", "File Monitor"};
@@ -142,6 +144,9 @@ LogManager::LogManager()
m_log[LogType::PROCESSORINTERFACE] = {"PI", "Processor Interface"};
m_log[LogType::POWERPC] = {"PowerPC", "PowerPC IBM CPU"};
m_log[LogType::SERIALINTERFACE] = {"SI", "Serial Interface"};
+ m_log[LogType::SERIALINTERFACE_AMBB] = {"SI_AMBB", "AMBB Interface"};
+ m_log[LogType::SERIALINTERFACE_CARD] = {"SI_CARD", "CARD Interface"};
+ m_log[LogType::SERIALINTERFACE_JVSIO] = {"SI_JVS", "JVS-I/O"};
m_log[LogType::SP1] = {"SP1", "Serial Port 1"};
m_log[LogType::SYMBOLS] = {"SYMBOLS", "Symbols"};
m_log[LogType::VIDEO] = {"Video", "Video Backend"};
diff --git a/Source/Core/Common/Network.cpp b/Source/Core/Common/Network.cpp
index 0ed2360267..d44d106da5 100644
--- a/Source/Core/Common/Network.cpp
+++ b/Source/Core/Common/Network.cpp
@@ -9,21 +9,234 @@
#include <vector>
#ifndef _WIN32
+#include <cstring>
#include <netinet/in.h>
-#include <string.h>
+#include <sys/socket.h>
#else
#include <WinSock2.h>
#endif
#include <fmt/format.h>
+#include <fmt/ranges.h>
#include "Common/BitUtils.h"
#include "Common/CommonFuncs.h"
+#include "Common/Logging/Log.h"
#include "Common/Random.h"
#include "Common/StringUtil.h"
+#include "Common/Swap.h"
namespace Common
{
+
+static constexpr auto GetMaskFromNetworkPrefixLength(u32 network_prefix_length)
+{
+ return (network_prefix_length == 0) ?
+ u32(0) :
+ u32(BigEndianValue(u32(-1) >> (32 - network_prefix_length)));
+}
+
+u32 IPv4Port::GetIPAddressValue() const
+{
+ return std::bit_cast<Common::BigEndianValue<u32>>(ip_address);
+}
+
+u16 IPv4Port::GetPortValue() const
+{
+ return std::bit_cast<Common::BigEndianValue<u16>>(port);
+}
+
+bool IPv4PortRange::IsMatch(IPv4Port subject) const
+{
+ const u32 ip_u32 = subject.GetIPAddressValue();
+ const u16 port_u16 = subject.GetPortValue();
+
+ return ip_u32 >= first.GetIPAddressValue() && ip_u32 <= last.GetIPAddressValue() &&
+ port_u16 >= first.GetPortValue() && port_u16 <= last.GetPortValue();
+}
+
+std::string IPv4PortRange::ToString() const
+{
+ const u32 first_ip_value = first.GetIPAddressValue();
+ const u32 last_ip_value = last.GetIPAddressValue();
+ const u32 different_bits = first_ip_value ^ last_ip_value;
+ const u32 common_high_bits = std::countl_zero(different_bits);
+
+ std::string ip_range = Common::IPAddressToString(first.ip_address);
+ if (common_high_bits == 32)
+ {
+ // Identical first and last IP.
+ }
+ else if ((last_ip_value - first_ip_value + 1) << common_high_bits == 0)
+ {
+ // An exact network range can use CIDR notation.
+ ip_range = fmt::format("{}/{}", ip_range, common_high_bits);
+ }
+ else if (common_high_bits >= 24)
+ {
+ // Only the last octet is different.
+ ip_range = fmt::format("{}-{}", ip_range, last.ip_address.back());
+ }
+ else
+ {
+ // Plainly specified range.
+ ip_range = fmt::format("{}-{}", ip_range, Common::IPAddressToString(last.ip_address));
+ }
+
+ if (first.port == 0)
+ return ip_range;
+
+ if (first.port == last.port)
+ return fmt::format("{}:{}", ip_range, first.GetPortValue());
+
+ return fmt::format("{}:{}-{}", ip_range, first.GetPortValue(), last.GetPortValue());
+}
+
+std::string IPAddressToString(IPAddress ip_address)
+{
+ return fmt::format("{}", fmt::join(ip_address, "."));
+}
+
+std::optional<IPv4PortRange> StringToIPv4PortRange(std::string_view subject)
+{
+ // TODO: This could be optimized and improved a lot.
+
+ auto read_pos = std::ranges::find_first_of(subject, std::string_view{"-/:"});
+
+ const auto first_ip_octets = SplitStringIntoArray<4>({subject.begin(), read_pos}, '.');
+
+ if (!first_ip_octets.has_value())
+ return std::nullopt; // Wrong number of octets.
+
+ std::optional<IPv4PortRange> result;
+ result.emplace();
+
+ for (u32 i = 0; i != result->first.ip_address.size(); ++i)
+ {
+ if (!TryParse(std::string((*first_ip_octets)[i]), &result->first.ip_address[i]))
+ return std::nullopt; // Bad octet.
+ }
+
+ if (read_pos == subject.end())
+ {
+ // Last IP and ports not specified.
+ result->last.ip_address = result->first.ip_address;
+ result->first.port = 0;
+ result->last.port = u16(-1);
+ return result;
+ }
+
+ if (*read_pos == '-')
+ {
+ // Read last IP.
+ ++read_pos;
+
+ const auto last_ip_end = std::ranges::find(read_pos, subject.end(), ':');
+ const auto last_ip_octets = SplitString({read_pos, last_ip_end}, '.');
+
+ // Copy octets from first_ip when less than 4 are specified.
+ // FYI: This currently allows for syntax like: 10.0.0.1-7.255
+ // I don't know if it's desirable or not to allow 2 or 3 octets on the last IP.
+ const std::size_t octet_share_count = 4 - last_ip_octets.size();
+
+ if (octet_share_count >= 4)
+ return std::nullopt; // Too many octets.
+
+ std::copy_n(result->first.ip_address.begin(), octet_share_count,
+ result->last.ip_address.begin());
+
+ std::size_t i = octet_share_count;
+ for (auto&& octet_str : last_ip_octets)
+ {
+ if (!TryParse(octet_str, &result->last.ip_address[i]))
+ return std::nullopt; // Bad octet.
+ ++i;
+ }
+
+ if (result->first.ip_address > result->last.ip_address)
+ return std::nullopt; // Reversed range.
+
+ read_pos = last_ip_end;
+ }
+ else if (*read_pos == '/')
+ {
+ // Read network prefix length.
+ ++read_pos;
+
+ u32 network_prefix_length{};
+ const auto [parse_end, ec] = FromChars({read_pos, subject.end()}, network_prefix_length);
+
+ if (ec != std::errc{} || network_prefix_length > 32)
+ return std::nullopt; // Bad network prefix.
+
+ read_pos += parse_end - std::to_address(read_pos); // Working around MSVC iterators..
+
+ const auto net_mask = GetMaskFromNetworkPrefixLength(network_prefix_length);
+ const auto first_ip_address_u32 = std::bit_cast<u32>(result->first.ip_address) & net_mask;
+ result->first.ip_address = std::bit_cast<IPAddress>(first_ip_address_u32);
+ result->last.ip_address = std::bit_cast<IPAddress>(first_ip_address_u32 | ~net_mask);
+ }
+ else
+ {
+ // Last IP not specified.
+ result->last.ip_address = result->first.ip_address;
+ }
+
+ if (read_pos == subject.end())
+ {
+ // Ports not specified.
+ result->first.port = 0;
+ result->last.port = u16(-1);
+ return result;
+ }
+
+ if (*read_pos != ':')
+ return std::nullopt; // Unexpected character.
+
+ u16 first_port_u16{};
+ {
+ // Read first port.
+ ++read_pos;
+
+ const auto [parse_end, ec] = FromChars({read_pos, subject.end()}, first_port_u16);
+ if (ec != std::errc{})
+ return std::nullopt; // Bad first port.
+
+ result->first.port = std::bit_cast<u16>(Common::BigEndianValue(first_port_u16));
+
+ read_pos += parse_end - std::to_address(read_pos); // Working around MSVC iterators..
+ }
+
+ if (read_pos == subject.end())
+ {
+ // Last port not specified.
+ result->last.port = result->first.port;
+ return result;
+ }
+
+ if (*read_pos != '-')
+ return std::nullopt; // Unexpected character.
+
+ u16 first_last_u16{};
+ {
+ // Read last port.
+ ++read_pos;
+
+ const auto [parse_end, ec] = FromChars({read_pos, subject.end()}, first_last_u16);
+ if (ec != std::errc{} || first_last_u16 < first_port_u16)
+ return std::nullopt; // Bad last port.
+
+ result->last.port = std::bit_cast<u16>(Common::BigEndianValue(first_last_u16));
+
+ read_pos += parse_end - std::to_address(read_pos); // Working around MSVC iterators..
+ }
+
+ if (read_pos != subject.end())
+ return std::nullopt;
+
+ return result;
+}
+
MACAddress GenerateMacAddress(const MACConsumer type)
{
constexpr std::array<u8, 3> oui_bba{{0x00, 0x09, 0xbf}};
@@ -586,4 +799,18 @@ const char* StrNetworkError()
#endif
return DecodeNetworkError(error_code);
}
+
+bool SetPlatformSocketOptions(int fd [[maybe_unused]])
+{
+#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
+ int opt_no_sigpipe = 1;
+ if (setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &opt_no_sigpipe, sizeof(opt_no_sigpipe)) < 0)
+ {
+ ERROR_LOG_FMT(COMMON, "Failed to set SO_NOSIGPIPE on socket: {}", StrNetworkError());
+ return false;
+ }
+#endif
+ return true;
+}
+
} // namespace Common
diff --git a/Source/Core/Common/Network.h b/Source/Core/Common/Network.h
index 8f446d7a38..5e0da6559e 100644
--- a/Source/Core/Common/Network.h
+++ b/Source/Core/Common/Network.h
@@ -10,6 +10,10 @@
#include <type_traits>
#include <vector>
+#if defined(__linux__) || defined(__HAIKU__)
+#include <sys/socket.h>
+#endif
+
#include "Common/CommonTypes.h"
struct sockaddr_in;
@@ -264,6 +268,30 @@ struct NetworkErrorState
#endif
};
+struct IPv4Port
+{
+ IPAddress ip_address;
+ u16 port; // Network byte order.
+
+ // These convert to host byte order.
+ u32 GetIPAddressValue() const;
+ u16 GetPortValue() const;
+};
+
+struct IPv4PortRange
+{
+ IPv4Port first;
+ IPv4Port last;
+
+ bool IsMatch(IPv4Port subject) const;
+ std::string ToString() const;
+};
+
+std::string IPAddressToString(IPAddress ip_address);
+
+// Syntax is: first_ip[-last_ip|/network_prefix_length][:first_port[-last_port]]
+std::optional<IPv4PortRange> StringToIPv4PortRange(std::string_view subject);
+
MACAddress GenerateMacAddress(MACConsumer type);
std::string MacAddressToString(const MACAddress& mac);
@@ -279,4 +307,15 @@ NetworkErrorState SaveNetworkErrorState();
void RestoreNetworkErrorState(const NetworkErrorState& state);
const char* DecodeNetworkError(s32 error_code);
const char* StrNetworkError();
+
+// Sets SO_NOSIGPIPE when available.
+bool SetPlatformSocketOptions(int fd);
+
+// Pass this to all `send` calls to avoid SIGPIPE.
+#if defined(__linux__) || defined(__HAIKU__)
+static constexpr int SEND_FLAGS = MSG_NOSIGNAL;
+#else
+static constexpr int SEND_FLAGS = 0;
+#endif
+
} // namespace Common
diff --git a/Source/Core/Common/StringUtil.h b/Source/Core/Common/StringUtil.h
index ea82581355..6e2c385df9 100644
--- a/Source/Core/Common/StringUtil.h
+++ b/Source/Core/Common/StringUtil.h
@@ -12,6 +12,7 @@
#include <iomanip>
#include <limits>
#include <locale>
+#include <ranges>
#include <span>
#include <sstream>
#include <string>
@@ -182,6 +183,32 @@ std::from_chars_result FromChars(std::string_view sv, std::floating_point auto&
std::vector<std::string> SplitString(const std::string& str, char delim);
+// Returns `nullopt` if subject does not contain exactly (Count - 1) delim.
+template <std::size_t Count>
+requires(Count > 1)
+std::optional<std::array<std::string_view, Count>> SplitStringIntoArray(std::string_view subject,
+ char delim)
+{
+ std::optional<std::array<std::string_view, Count>> result;
+ result.emplace();
+
+ std::size_t index = 0;
+ for (auto&& item : subject | std::views::split(delim))
+ {
+ // Too many delim.
+ if (index == Count)
+ return std::nullopt;
+
+ (*result)[index++] = std::string_view{item};
+ }
+
+ // Too few delim.
+ if (index != Count)
+ return std::nullopt;
+
+ return result;
+}
+
// "C:/Windows/winhelp.exe" to "C:/Windows/", "winhelp", ".exe"
// This requires forward slashes to be used for the path separators, even on Windows.
bool SplitPath(std::string_view full_path, std::string* path, std::string* filename,
diff --git a/Source/Core/Common/UnixUtil.h b/Source/Core/Common/UnixUtil.h
index f1b5fc136f..374126e403 100644
--- a/Source/Core/Common/UnixUtil.h
+++ b/Source/Core/Common/UnixUtil.h
@@ -4,13 +4,18 @@
#pragma once
#include <pthread.h>
+
+#if defined(__linux__)
#include <sys/eventfd.h>
+#endif
#include "Common/CommonFuncs.h"
#include "Common/Logging/Log.h"
namespace UnixUtil
{
+
+#if defined(__linux__)
inline int CreateEventFD(unsigned int count, int flags)
{
const int result = eventfd(count, flags);
@@ -21,6 +26,7 @@ inline int CreateEventFD(unsigned int count, int flags)
}
return result;
}
+#endif
// Repeatedly call a function that can erroneously produce EINTR.
auto RetryOnEINTR(auto func, auto... args)
diff --git a/Source/Core/Core/AchievementManager.h b/Source/Core/Core/AchievementManager.h
index b616dded44..2ab23a4368 100644
--- a/Source/Core/Core/AchievementManager.h
+++ b/Source/Core/Core/AchievementManager.h
@@ -83,8 +83,8 @@ public:
static constexpr std::string_view BLUE = "#0B71C1";
static constexpr std::string_view APPROVED_LIST_FILENAME = "ApprovedInis.json";
static const inline Common::SHA1::Digest APPROVED_LIST_HASH = {
- 0x7D, 0x96, 0x3E, 0x00, 0x30, 0x1C, 0x7D, 0x0E, 0x72, 0x09,
- 0xE1, 0xF0, 0xF3, 0x51, 0x6D, 0x5C, 0xB3, 0x68, 0xAD, 0x79};
+ 0xEA, 0x2F, 0x74, 0xA1, 0x6C, 0xF3, 0xB5, 0xD4, 0x8A, 0xAF,
+ 0x03, 0x30, 0x58, 0x2A, 0xE0, 0xF7, 0x0A, 0x88, 0x86, 0xB3};
struct LeaderboardEntry
{
diff --git a/Source/Core/Core/Boot/Boot.cpp b/Source/Core/Core/Boot/Boot.cpp
index 1f87d06281..2ccf0cdcc4 100644
--- a/Source/Core/Core/Boot/Boot.cpp
+++ b/Source/Core/Core/Boot/Boot.cpp
@@ -36,6 +36,7 @@
#include "Core/ConfigManager.h"
#include "Core/FifoPlayer/FifoPlayer.h"
#include "Core/HLE/HLE.h"
+#include "Core/HW/DVD/AMMediaboard.h"
#include "Core/HW/DVD/DVDInterface.h"
#include "Core/HW/EXI/EXI_DeviceIPL.h"
#include "Core/HW/Memmap.h"
@@ -373,7 +374,8 @@ bool CBoot::Load_BS2(Core::System& system, const std::string& boot_rom_filename)
constexpr u32 PAL_v1_0 = 0x4F319F43;
// DOL-101(EUR) (PAL Revision 1.2)
constexpr u32 PAL_v1_2 = 0xAD1B7F16;
- constexpr u32 Triforce = 0xD1883221; // The Triforce's special IPL
+ // Triforce Arcade IPL (DEV Revision 1.0)
+ constexpr u32 Triforce = 0xD1883221;
// Load the IPL ROM dump, limited to 2MiB which is the size of the official IPLs.
constexpr size_t max_ipl_size = 2 * 1024 * 1024;
@@ -407,6 +409,7 @@ bool CBoot::Load_BS2(Core::System& system, const std::string& boot_rom_filename)
break;
case Triforce:
known_ipl = true;
+ break;
default:
PanicAlertFmtT("The IPL file is not a known good dump. (CRC32: {0:x})", ipl_hash);
break;
@@ -457,6 +460,14 @@ bool CBoot::Load_BS2(Core::System& system, const std::string& boot_rom_filename)
ppc_state.spr[SPR_DBAT3L] = 0xfff00001;
SetupBAT(system, /*is_wii*/ false);
+ if (ipl_hash == Triforce)
+ {
+ HLE::Patch(system, 0x813048B8, "OSReport");
+ HLE::Patch(system, 0x8130095C, "OSReport"); // Apploader
+
+ AMMediaboard::FirmwareMap(true);
+ }
+
ppc_state.pc = 0x81200150;
system.GetPowerPC().MSRUpdated();
@@ -490,6 +501,12 @@ bool CBoot::BootUp(Core::System& system, const Core::CPUThreadGuard& guard,
{
SConfig& config = SConfig::GetInstance();
+ // Triforce systems are region free and always must use the NTSC video mode
+ if (system.IsTriforce())
+ {
+ config.m_region = DiscIO::Region::NTSC_J;
+ }
+
// PAL Wii uses NTSC framerate and linecount in 60Hz modes
system.GetVideoInterface().Preset(DiscIO::IsNTSC(config.m_region) ||
(system.IsWii() && Config::Get(Config::SYSCONF_PAL60)));
diff --git a/Source/Core/Core/Boot/Boot_BS2Emu.cpp b/Source/Core/Core/Boot/Boot_BS2Emu.cpp
index 3f88b29e3b..a44796dd4f 100644
--- a/Source/Core/Core/Boot/Boot_BS2Emu.cpp
+++ b/Source/Core/Core/Boot/Boot_BS2Emu.cpp
@@ -21,6 +21,7 @@
#include "Core/Core.h"
#include "Core/Debugger/BranchWatch.h"
#include "Core/HLE/HLE.h"
+#include "Core/HW/DVD/AMMediaboard.h"
#include "Core/HW/DVD/DVDInterface.h"
#include "Core/HW/EXI/EXI_DeviceIPL.h"
#include "Core/HW/Memmap.h"
@@ -228,12 +229,16 @@ bool CBoot::RunApploader(Core::System& system, const Core::CPUThreadGuard& guard
ppc_state.pc = ppc_state.gpr[3];
branch_watch.SetRecordingActive(guard, resume_branch_watch);
- // Blank out session key (https://debugmo.de/2008/05/part-2-dumping-the-media-board/)
- if (volume.GetVolumeType() == DiscIO::Platform::Triforce)
+
+ if (system.IsTriforce())
{
auto& memory = system.GetMemory();
- memory.Memset(0, 0, 12);
+ AMMediaboard::InitDIMM(volume);
+
+ // Triforce disc register obfuscation
+ AMMediaboard::InitKeys(memory.Read_U32(0), memory.Read_U32(4), memory.Read_U32(8));
+ AMMediaboard::FirmwareMap(false);
}
return true;
diff --git a/Source/Core/Core/BootManager.cpp b/Source/Core/Core/BootManager.cpp
index ae49a14b92..85444ef0b6 100644
--- a/Source/Core/Core/BootManager.cpp
+++ b/Source/Core/Core/BootManager.cpp
@@ -25,8 +25,8 @@
#include "Core/AchievementManager.h"
#include "Core/Boot/Boot.h"
+#include "Core/Config/GraphicsSettings.h"
#include "Core/Config/MainSettings.h"
-#include "Core/Config/SYSCONFSettings.h"
#include "Core/ConfigLoaders/BaseConfigLoader.h"
#include "Core/ConfigLoaders/NetPlayConfigLoader.h"
#include "Core/ConfigManager.h"
@@ -118,7 +118,7 @@ bool BootCore(Core::System& system, std::unique_ptr<BootParameters> boot,
case DiscIO::Region::NTSC_K:
Config::SetCurrent(Config::SYSCONF_COUNTRY, 0x88); // South Korea
break;
- case DiscIO::Region::Unknown:
+ default:
break;
}
}
@@ -134,10 +134,34 @@ bool BootCore(Core::System& system, std::unique_ptr<BootParameters> boot,
if (!boot->riivolution_patches.empty())
Config::SetCurrent(Config::MAIN_FAST_DISC_SPEED, true);
+ if (system.IsTriforce())
+ {
+ // Attach Triforce Baseboard hardware if not overridden in any layer.
+ // Users may set these in their GameConfig if they want them not automatically attached.
+ if (GetActiveLayerForConfig(Config::MAIN_SERIAL_PORT_1) == Config::LayerType::Base)
+ {
+ Config::SetCurrent(Config::MAIN_SERIAL_PORT_1, ExpansionInterface::EXIDeviceType::Baseboard);
+ }
+ if (GetActiveLayerForConfig(Config::GetInfoForSIDevice(0)) == Config::LayerType::Base)
+ {
+ Config::SetCurrent(Config::GetInfoForSIDevice(0),
+ SerialInterface::SIDevices::SIDEVICE_AM_BASEBOARD);
+ }
+
+ // Mario Kart Arcade GP has widescreen heuristic issues.
+ // All Triforce games are 4:3 so we'll just disable the heuristic for now.
+ if (GetActiveLayerForConfig(Config::GFX_SUGGESTED_ASPECT_RATIO) == Config::LayerType::Base)
+ {
+ Config::SetCurrent(Config::GFX_SUGGESTED_ASPECT_RATIO, AspectMode::ForceStandard);
+ }
+ }
+
system.Initialize();
Core::UpdateWantDeterminism(system, /*initial*/ true);
+ ConfigLoaders::TransferSYSCONFControlToGuest();
+
if (system.IsWii())
{
Core::InitializeWiiRoot(Core::WantsDeterminism());
@@ -146,13 +170,16 @@ bool BootCore(Core::System& system, std::unique_ptr<BootParameters> boot,
if (!Core::WantsDeterminism())
{
Core::BackupWiiSettings();
- ConfigLoaders::SaveToSYSCONF(Config::LayerType::Meta);
+ ConfigLoaders::SaveToSYSCONF(Config::LayerType::Meta,
+ ConfigLoaders::SkipIfControlledByGuest::No);
}
else
{
- ConfigLoaders::SaveToSYSCONF(Config::LayerType::Meta, [](const Config::Location& location) {
- return Config::GetActiveLayerForConfig(location) >= Config::LayerType::Movie;
- });
+ ConfigLoaders::SaveToSYSCONF(
+ Config::LayerType::Meta, ConfigLoaders::SkipIfControlledByGuest::No,
+ [](const Config::Location& location) {
+ return Config::GetActiveLayerForConfig(location) >= Config::LayerType::Movie;
+ });
}
}
@@ -173,43 +200,18 @@ bool BootCore(Core::System& system, std::unique_ptr<BootParameters> boot,
return Core::Init(system, std::move(boot), wsi);
}
-// SYSCONF can be modified during emulation by the user and internally, which makes it
-// a bad idea to just always overwrite it with the settings from the base layer.
-//
-// Conversely, we also shouldn't just accept any changes to SYSCONF, as it may cause
-// temporary settings (from Movie, Netplay, game INIs, etc.) to stick around.
-//
-// To avoid inconveniences in most cases, we accept changes that aren't being overridden by a
-// non-base layer, and restore only the overridden settings.
-static void RestoreSYSCONF()
-{
- // This layer contains the new SYSCONF settings (including any temporary settings).
- Config::Layer temp_layer(Config::LayerType::Base);
- // Use a separate loader so the temp layer doesn't automatically save
- ConfigLoaders::GenerateBaseConfigLoader()->Load(&temp_layer);
-
- for (const auto& setting : Config::SYSCONF_SETTINGS)
- {
- std::visit(
- [&](auto* info) {
- // If this setting was overridden, then we copy the base layer value back to the SYSCONF.
- // Otherwise we leave the new value in the SYSCONF.
- if (Config::GetActiveLayerForConfig(*info) == Config::LayerType::Base)
- Config::SetBase(*info, temp_layer.Get(*info));
- },
- setting.config_info);
- }
- ConfigLoaders::SaveToSYSCONF(Config::LayerType::Base);
-}
-
void RestoreConfig()
{
Core::ShutdownWiiRoot();
- if (!Core::WiiRootIsTemporary())
+ if (Core::WiiRootIsTemporary())
+ {
+ ConfigLoaders::TransferSYSCONFControlFromGuest(ConfigLoaders::WriteBackChangedValues::No);
+ }
+ else
{
Core::RestoreWiiSettings(Core::RestoreReason::EmulationEnd);
- RestoreSYSCONF();
+ ConfigLoaders::TransferSYSCONFControlFromGuest(ConfigLoaders::WriteBackChangedValues::Yes);
}
Config::ClearCurrentRunLayer();
diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt
index 85e446e2a0..e2ae3e9919 100644
--- a/Source/Core/Core/CMakeLists.txt
+++ b/Source/Core/Core/CMakeLists.txt
@@ -181,6 +181,8 @@ add_library(core
HW/DSPLLE/DSPSymbols.h
HW/DVD/DVDInterface.cpp
HW/DVD/DVDInterface.h
+ HW/DVD/AMMediaboard.cpp
+ HW/DVD/AMMediaboard.h
HW/DVD/DVDMath.cpp
HW/DVD/DVDMath.h
HW/DVD/DVDThread.cpp
@@ -252,6 +254,12 @@ add_library(core
HW/HSP/HSP_DeviceNull.h
HW/HW.cpp
HW/HW.h
+ HW/MagCard/C1231BR.cpp
+ HW/MagCard/C1231BR.h
+ HW/MagCard/C1231LR.cpp
+ HW/MagCard/C1231LR.h
+ HW/MagCard/MagneticCardReader.cpp
+ HW/MagCard/MagneticCardReader.h
HW/Memmap.cpp
HW/Memmap.h
HW/MemoryInterface.cpp
@@ -274,6 +282,8 @@ add_library(core
HW/SI/SI_DeviceGCSteeringWheel.h
HW/SI/SI_DeviceKeyboard.cpp
HW/SI/SI_DeviceKeyboard.h
+ HW/SI/SI_DeviceAMBaseboard.cpp
+ HW/SI/SI_DeviceAMBaseboard.h
HW/SI/SI_DeviceNull.cpp
HW/SI/SI_DeviceNull.h
HW/SI/SI.cpp
diff --git a/Source/Core/Core/CheatSearch.cpp b/Source/Core/Core/CheatSearch.cpp
index 2cf509e880..063a09a582 100644
--- a/Source/Core/Core/CheatSearch.cpp
+++ b/Source/Core/Core/CheatSearch.cpp
@@ -284,6 +284,15 @@ void Cheats::CheatSearchSession<T>::ResetResults()
}
template <typename T>
+void Cheats::CheatSearchSession<T>::RemoveResult(size_t index)
+{
+ if (index < m_search_results.size())
+ {
+ m_search_results.erase(m_search_results.begin() + index);
+ }
+}
+
+template <typename T>
static std::function<bool(const T& new_value)>
MakeCompareFunctionForSpecificValue(Cheats::CompareType op, const T& old_value)
{
diff --git a/Source/Core/Core/CheatSearch.h b/Source/Core/Core/CheatSearch.h
index 7df135d51a..7d1c089835 100644
--- a/Source/Core/Core/CheatSearch.h
+++ b/Source/Core/Core/CheatSearch.h
@@ -168,6 +168,9 @@ public:
virtual bool WriteValue(const Core::CPUThreadGuard& guard, std::span<u32> addresses) const = 0;
+ // User can delete a search result.
+ virtual void RemoveResult(size_t index) = 0;
+
// Create a complete copy of this search session.
virtual std::unique_ptr<CheatSearchSessionBase> Clone() const = 0;
@@ -195,6 +198,7 @@ public:
bool SetValueFromString(const std::string& value_as_string, bool force_parse_as_hex) override;
void ResetResults() override;
+ void RemoveResult(size_t index) override;
SearchErrorCode RunSearch(const Core::CPUThreadGuard& guard) override;
size_t GetMemoryRangeCount() const override;
diff --git a/Source/Core/Core/Config/MainSettings.cpp b/Source/Core/Core/Config/MainSettings.cpp
index 8696a60417..441c5dd031 100644
--- a/Source/Core/Core/Config/MainSettings.cpp
+++ b/Source/Core/Core/Config/MainSettings.cpp
@@ -6,6 +6,7 @@
#include <sstream>
#include <fmt/format.h>
+#include <fmt/ranges.h>
#include "AudioCommon/AudioCommon.h"
#include "Common/Assert.h"
@@ -20,13 +21,16 @@
#include "Common/Version.h"
#include "Core/AchievementManager.h"
#include "Core/Config/DefaultLocale.h"
+#include "Core/Core.h"
#include "Core/HW/EXI/EXI.h"
#include "Core/HW/EXI/EXI_Device.h"
#include "Core/HW/GCMemcard/GCMemcard.h"
#include "Core/HW/HSP/HSP_Device.h"
#include "Core/HW/Memmap.h"
#include "Core/HW/SI/SI_Device.h"
+#include "Core/IOS/Network/Socket.h"
#include "Core/PowerPC/PowerPC.h"
+#include "Core/System.h"
#include "Core/USBUtils.h"
#include "DiscIO/Enums.h"
#include "VideoCommon/VideoBackendBase.h"
@@ -656,6 +660,45 @@ const std::array<Info<s16>, EMULATED_LOGITECH_MIC_COUNT> MAIN_LOGITECH_MIC_VOLUM
Info<s16>{{System::Main, "EmulatedUSBDevices", "LogitechMic3VolumeModifier"}, 0},
Info<s16>{{System::Main, "EmulatedUSBDevices", "LogitechMic4VolumeModifier"}, 0}};
+static std::string GetDefaultTriforceIPRedirections()
+{
+#if defined(ANDROID)
+ return "0.0.0.0/0=127.0.0.1";
+#else
+ constexpr std::string_view entries[] = {
+
+ // Mario Kart Arcade GP 1 + 2
+ // This config allows for 4x Multicabinet on the same PC.
+ "192.168.29.150:5000-5008=127.0.0.1:5000-5008 MarioKart Seat #1 Static",
+ "192.168.29.151:5000-5008=127.0.0.1:5010-5018 MarioKart Seat #2 Static",
+ "192.168.29.152:5000-5008=127.0.0.1:5020-5028 MarioKart Seat #3 Static",
+ "192.168.29.153:5000-5008=127.0.0.1:5030-5038 MarioKart Seat #4 Static",
+ // Ephemeral ports are constrained to differentiate incoming connections.
+ "192.168.29.150=127.0.0.1:50000-50999 MarioKart Seat #1 Ephemeral",
+ "192.168.29.151=127.0.0.1:51000-51999 MarioKart Seat #2 Ephemeral",
+ "192.168.29.152=127.0.0.1:52000-52999 MarioKart Seat #3 Ephemeral",
+ "192.168.29.153=127.0.0.1:53000-53999 MarioKart Seat #4 Ephemeral",
+ // The cameras.
+ "192.168.29.104-107=127.0.0.1 MarioKart namcam2",
+
+ // CyCraft Connect IP
+ "192.168.11.0/24=127.0.0.1 CyCraft",
+
+ // The Key of Avalon
+ // This config isn't usable as-is. It's just here for reference.
+ "192.168.13.0/24=127.0.0.1 The Key of Avalon",
+
+ // Sega ALL.Net
+ "192.168.150.0/24=127.0.0.1 ALL.Net",
+ };
+
+ return fmt::format("{}", fmt::join(entries, ","));
+#endif
+}
+
+const Info<std::string> MAIN_TRIFORCE_IP_REDIRECTIONS{
+ {System::Main, "Core", "TriforceIPRedirections"}, GetDefaultTriforceIPRedirections()};
+
// The reason we need this function is because some memory card code
// expects to get a non-NTSC-K region even if we're emulating an NTSC-K Wii.
DiscIO::Region ToGameCubeRegion(DiscIO::Region region)
@@ -689,6 +732,9 @@ const char* GetDirectoryForRegion(DiscIO::Region region, RegionDirectoryStyle st
ASSERT_MSG(BOOT, false, "NTSC-K is not a valid GameCube region");
return style == RegionDirectoryStyle::Legacy ? JAP_DIR : JPN_DIR;
+ case DiscIO::Region::DEV:
+ return DEV_DIR;
+
default:
ASSERT_MSG(BOOT, false, "Default case should not be reached");
return EUR_DIR;
@@ -742,6 +788,7 @@ std::string GetMemcardPath(std::string configured_filename, ExpansionInterface::
constexpr std::string_view us_region = "." USA_DIR;
constexpr std::string_view jp_region = "." JAP_DIR;
constexpr std::string_view eu_region = "." EUR_DIR;
+ constexpr std::string_view dv_region = "." DEV_DIR;
std::optional<DiscIO::Region> path_region = std::nullopt;
if (name.ends_with(us_region))
{
@@ -758,6 +805,11 @@ std::string GetMemcardPath(std::string configured_filename, ExpansionInterface::
name = name.substr(0, name.size() - eu_region.size());
path_region = DiscIO::Region::PAL;
}
+ else if (name.ends_with(dv_region))
+ {
+ name = name.substr(0, name.size() - dv_region.size());
+ path_region = DiscIO::Region::DEV;
+ }
const DiscIO::Region used_region =
region ? *region : (path_region ? *path_region : Config::Get(Config::MAIN_FALLBACK_REGION));
@@ -801,6 +853,7 @@ std::string GetGCIFolderPath(std::string configured_folder, ExpansionInterface::
constexpr std::string_view us_region = "/" USA_DIR;
constexpr std::string_view jp_region = "/" JPN_DIR;
constexpr std::string_view eu_region = "/" EUR_DIR;
+ constexpr std::string_view dv_region = "/" DEV_DIR;
std::string_view base_path = configured_folder;
std::optional<DiscIO::Region> path_region = std::nullopt;
if (base_path.ends_with(us_region))
@@ -818,6 +871,11 @@ std::string GetGCIFolderPath(std::string configured_folder, ExpansionInterface::
base_path = base_path.substr(0, base_path.size() - eu_region.size());
path_region = DiscIO::Region::PAL;
}
+ else if (base_path.ends_with(dv_region))
+ {
+ base_path = base_path.substr(0, base_path.size() - dv_region.size());
+ path_region = DiscIO::Region::DEV;
+ }
const DiscIO::Region used_region =
region ? *region : (path_region ? *path_region : Config::Get(Config::MAIN_FALLBACK_REGION));
diff --git a/Source/Core/Core/Config/MainSettings.h b/Source/Core/Core/Config/MainSettings.h
index 6a694e64cf..1946045e48 100644
--- a/Source/Core/Core/Config/MainSettings.h
+++ b/Source/Core/Core/Config/MainSettings.h
@@ -393,6 +393,8 @@ extern const std::array<Info<std::string>, EMULATED_LOGITECH_MIC_COUNT>
extern const std::array<Info<bool>, EMULATED_LOGITECH_MIC_COUNT> MAIN_LOGITECH_MIC_MUTED;
extern const std::array<Info<s16>, EMULATED_LOGITECH_MIC_COUNT> MAIN_LOGITECH_MIC_VOLUME_MODIFIER;
+extern const Info<std::string> MAIN_TRIFORCE_IP_REDIRECTIONS;
+
// GameCube path utility functions
// Replaces NTSC-K with some other region, and doesn't replace non-NTSC-K regions
diff --git a/Source/Core/Core/ConfigLoaders/BaseConfigLoader.cpp b/Source/Core/Core/ConfigLoaders/BaseConfigLoader.cpp
index 811eb9974e..8b35d4d684 100644
--- a/Source/Core/Core/ConfigLoaders/BaseConfigLoader.cpp
+++ b/Source/Core/Core/ConfigLoaders/BaseConfigLoader.cpp
@@ -8,11 +8,13 @@
#include <functional>
#include <map>
#include <memory>
+#include <mutex>
#include <string>
#include <variant>
#include "Common/CommonTypes.h"
#include "Common/Config/Config.h"
+#include "Common/Config/Layer.h"
#include "Common/FileUtil.h"
#include "Common/IniFile.h"
#include "Common/Logging/Log.h"
@@ -21,17 +23,68 @@
#include "Core/Config/SYSCONFSettings.h"
#include "Core/ConfigLoaders/IsSettingSaveable.h"
#include "Core/ConfigManager.h"
-#include "Core/Core.h"
#include "Core/IOS/IOS.h"
#include "Core/IOS/USB/Bluetooth/BTBase.h"
#include "Core/SysConf.h"
#include "Core/System.h"
+static bool s_sysconf_controlled_by_guest = false;
+static std::recursive_mutex s_sysconf_lock;
+
namespace ConfigLoaders
{
-void SaveToSYSCONF(Config::LayerType layer, std::function<bool(const Config::Location&)> predicate)
+void TransferSYSCONFControlToGuest()
+{
+ std::lock_guard lock(s_sysconf_lock);
+
+ ASSERT(!s_sysconf_controlled_by_guest);
+ s_sysconf_controlled_by_guest = true;
+}
+
+void TransferSYSCONFControlFromGuest(WriteBackChangedValues write_back_changed_values)
+{
+ std::lock_guard lock(s_sysconf_lock);
+
+ ASSERT(s_sysconf_controlled_by_guest);
+ s_sysconf_controlled_by_guest = false;
+
+ if (write_back_changed_values == WriteBackChangedValues::No)
+ return;
+
+ // SYSCONF can be modified during emulation by the user and internally, which makes it
+ // a bad idea to just always overwrite it with the settings from the base layer.
+ //
+ // Conversely, we also shouldn't just accept any changes to SYSCONF, as it may cause
+ // temporary settings (from Movie, Netplay, game INIs, etc.) to stick around.
+ //
+ // To avoid inconveniences in most cases, we accept changes that aren't being overridden by a
+ // non-base layer, and restore only the overridden settings.
+
+ // This layer contains the new SYSCONF settings (including any temporary settings).
+ Config::Layer temp_layer(Config::LayerType::Base);
+ // Use a separate loader so the temp layer doesn't automatically save
+ GenerateBaseConfigLoader()->Load(&temp_layer);
+
+ for (const auto& setting : Config::SYSCONF_SETTINGS)
+ {
+ std::visit(
+ [&](auto* info) {
+ // If this setting was overridden, then we copy the base layer value back to the
+ // SYSCONF. Otherwise we leave the new value in the SYSCONF.
+ if (Config::GetActiveLayerForConfig(*info) == Config::LayerType::Base)
+ Config::SetBase(*info, temp_layer.Get(*info));
+ },
+ setting.config_info);
+ }
+
+ SaveToSYSCONF(Config::LayerType::Base, SkipIfControlledByGuest::No);
+}
+
+void SaveToSYSCONF(Config::LayerType layer, SkipIfControlledByGuest skip,
+ std::function<bool(const Config::Location&)> predicate)
{
- if (Core::IsRunning(Core::System::GetInstance()))
+ std::lock_guard lock(s_sysconf_lock);
+ if (skip == SkipIfControlledByGuest::Yes && s_sysconf_controlled_by_guest)
return;
IOS::HLE::Kernel ios;
@@ -125,7 +178,7 @@ public:
void Save(Config::Layer* layer) override
{
- SaveToSYSCONF(layer->GetLayer());
+ SaveToSYSCONF(layer->GetLayer(), SkipIfControlledByGuest::Yes);
std::map<Config::System, Common::IniFile> inis;
@@ -180,7 +233,8 @@ public:
private:
void LoadFromSYSCONF(Config::Layer* layer)
{
- if (Core::IsRunning(Core::System::GetInstance()))
+ std::lock_guard lock(s_sysconf_lock);
+ if (s_sysconf_controlled_by_guest)
return;
IOS::HLE::Kernel ios;
diff --git a/Source/Core/Core/ConfigLoaders/BaseConfigLoader.h b/Source/Core/Core/ConfigLoaders/BaseConfigLoader.h
index 7c20a5e065..30a6d26e4b 100644
--- a/Source/Core/Core/ConfigLoaders/BaseConfigLoader.h
+++ b/Source/Core/Core/ConfigLoaders/BaseConfigLoader.h
@@ -15,7 +15,21 @@ struct Location;
namespace ConfigLoaders
{
-void SaveToSYSCONF(Config::LayerType layer,
+enum class WriteBackChangedValues
+{
+ No,
+ Yes,
+};
+
+enum class SkipIfControlledByGuest
+{
+ No,
+ Yes,
+};
+
+void TransferSYSCONFControlToGuest();
+void TransferSYSCONFControlFromGuest(WriteBackChangedValues write_back_changed_values);
+void SaveToSYSCONF(Config::LayerType layer, SkipIfControlledByGuest skip,
std::function<bool(const Config::Location&)> predicate = {});
std::unique_ptr<Config::ConfigLayerLoader> GenerateBaseConfigLoader();
} // namespace ConfigLoaders
diff --git a/Source/Core/Core/ConfigManager.cpp b/Source/Core/Core/ConfigManager.cpp
index 4268b41481..8404cfff07 100644
--- a/Source/Core/Core/ConfigManager.cpp
+++ b/Source/Core/Core/ConfigManager.cpp
@@ -142,12 +142,6 @@ const std::string SConfig::GetTitleDescription() const
return m_title_description;
}
-std::string SConfig::GetTriforceID() const
-{
- std::lock_guard<std::recursive_mutex> lock(m_metadata_lock);
- return m_triforce_id;
-}
-
u64 SConfig::GetTitleID() const
{
std::lock_guard<std::recursive_mutex> lock(m_metadata_lock);
@@ -163,7 +157,7 @@ u16 SConfig::GetRevision() const
void SConfig::ResetRunningGameMetadata()
{
std::lock_guard<std::recursive_mutex> lock(m_metadata_lock);
- SetRunningGameMetadata("00000000", "", "", 0, 0, DiscIO::Region::Unknown);
+ SetRunningGameMetadata("00000000", "", 0, 0, DiscIO::Region::Unknown);
}
void SConfig::SetRunningGameMetadata(const DiscIO::Volume& volume,
@@ -172,14 +166,14 @@ void SConfig::SetRunningGameMetadata(const DiscIO::Volume& volume,
std::lock_guard<std::recursive_mutex> lock(m_metadata_lock);
if (partition == volume.GetGamePartition())
{
- SetRunningGameMetadata(volume.GetGameID(), volume.GetGameTDBID(), volume.GetTriforceID(),
+ SetRunningGameMetadata(volume.GetGameID(), volume.GetGameTDBID(),
volume.GetTitleID().value_or(0), volume.GetRevision().value_or(0),
volume.GetRegion());
}
else
{
SetRunningGameMetadata(volume.GetGameID(partition), volume.GetGameTDBID(partition),
- volume.GetTriforceID(), volume.GetTitleID(partition).value_or(0),
+ volume.GetTitleID(partition).value_or(0),
volume.GetRevision(partition).value_or(0), volume.GetRegion());
}
}
@@ -197,28 +191,25 @@ void SConfig::SetRunningGameMetadata(const IOS::ES::TMDReader& tmd, DiscIO::Plat
!Core::System::GetInstance().GetDVDInterface().UpdateRunningGameMetadata(tmd_title_id))
{
// If not launching a disc game, just read everything from the TMD.
- SetRunningGameMetadata(tmd.GetGameID(), tmd.GetGameTDBID(), "", tmd_title_id,
- tmd.GetTitleVersion(), tmd.GetRegion());
+ SetRunningGameMetadata(tmd.GetGameID(), tmd.GetGameTDBID(), tmd_title_id, tmd.GetTitleVersion(),
+ tmd.GetRegion());
}
}
void SConfig::SetRunningGameMetadata(const std::string& game_id)
{
std::lock_guard<std::recursive_mutex> lock(m_metadata_lock);
- SetRunningGameMetadata(game_id, "", "", 0, 0, DiscIO::Region::Unknown);
+ SetRunningGameMetadata(game_id, "", 0, 0, DiscIO::Region::Unknown);
}
void SConfig::SetRunningGameMetadata(const std::string& game_id, const std::string& gametdb_id,
- std::string triforce_id, u64 title_id, u16 revision,
- DiscIO::Region region)
+ u64 title_id, u16 revision, DiscIO::Region region)
{
std::lock_guard<std::recursive_mutex> lock(m_metadata_lock);
const bool was_changed = m_game_id != game_id || m_gametdb_id != gametdb_id ||
- m_triforce_id != triforce_id || m_title_id != title_id ||
- m_revision != revision;
+ m_title_id != title_id || m_revision != revision;
m_game_id = game_id;
m_gametdb_id = gametdb_id;
- m_triforce_id = triforce_id;
m_title_id = title_id;
m_revision = revision;
@@ -251,7 +242,7 @@ void SConfig::SetRunningGameMetadata(const std::string& game_id, const std::stri
const Core::TitleDatabase title_database;
auto& system = Core::System::GetInstance();
const DiscIO::Language language = GetLanguageAdjustedForRegion(system.IsWii(), region);
- m_title_name = title_database.GetTitleName(m_gametdb_id, m_triforce_id, language);
+ m_title_name = title_database.GetTitleName(m_gametdb_id, language);
m_title_description = title_database.Describe(m_gametdb_id, language);
NOTICE_LOG_FMT(CORE, "Active title: {}", m_title_description);
Host_TitleChanged();
@@ -458,6 +449,9 @@ bool SConfig::SetPathsAndGameMetadata(Core::System& system, const BootParameters
if (m_region == DiscIO::Region::Unknown)
m_region = Config::Get(Config::MAIN_FALLBACK_REGION);
+ if (system.IsTriforce())
+ m_region = DiscIO::Region::DEV;
+
// Set up paths
const std::string region_dir = Config::GetDirectoryForRegion(Config::ToGameCubeRegion(m_region));
m_strSRAM = File::GetUserPath(F_GCSRAM_IDX);
diff --git a/Source/Core/Core/ConfigManager.h b/Source/Core/Core/ConfigManager.h
index b2e27892b9..39dbc6d39a 100644
--- a/Source/Core/Core/ConfigManager.h
+++ b/Source/Core/Core/ConfigManager.h
@@ -54,7 +54,6 @@ struct SConfig
// files
std::string m_strBootROM;
std::string m_strSRAM;
-
std::string m_debugger_game_id;
// TODO: remove this as soon as the ticket view hack in IOS/ES/Views is dropped.
@@ -64,7 +63,6 @@ struct SConfig
const std::string GetGameTDBID() const;
const std::string GetTitleName() const;
const std::string GetTitleDescription() const;
- std::string GetTriforceID() const;
u64 GetTitleID() const;
u16 GetRevision() const;
void ResetRunningGameMetadata();
@@ -121,15 +119,13 @@ private:
static void ReloadTextures(Core::System& system);
void SetRunningGameMetadata(const std::string& game_id, const std::string& gametdb_id,
- std::string triforce_id, u64 title_id, u16 revision,
- DiscIO::Region region);
+ u64 title_id, u16 revision, DiscIO::Region region);
static SConfig* m_Instance;
mutable std::recursive_mutex m_metadata_lock;
std::string m_game_id;
std::string m_gametdb_id;
- std::string m_triforce_id;
std::string m_title_name;
std::string m_title_description;
u64 m_title_id;
diff --git a/Source/Core/Core/Debugger/BranchWatch.h b/Source/Core/Core/Debugger/BranchWatch.h
index a4e5ce1775..f455d91898 100644
--- a/Source/Core/Core/Debugger/BranchWatch.h
+++ b/Source/Core/Core/Debugger/BranchWatch.h
@@ -12,7 +12,10 @@
#include <vector>
#include "Common/CommonTypes.h"
+#include "Core/Core.h"
#include "Core/PowerPC/Gekko.h"
+#include "Core/PowerPC/JitInterface.h"
+#include "Core/System.h"
namespace Core
{
@@ -110,7 +113,7 @@ enum class BranchWatchPhase : bool
Reduction,
};
-class BranchWatch final // Class is final to enforce the safety of GetOffsetOfRecordingActive().
+class BranchWatch final
{
public:
using Collection = BranchWatchCollection;
@@ -119,7 +122,12 @@ public:
using SelectionInspection = BranchWatchSelectionInspection;
bool GetRecordingActive() const { return m_recording_active; }
- void SetRecordingActive(const CPUThreadGuard& guard, bool active) { m_recording_active = active; }
+ void SetRecordingActive(const CPUThreadGuard& guard, bool active)
+ {
+ m_recording_active = active;
+ auto& system = guard.GetSystem();
+ system.GetJitInterface().ClearCache(guard);
+ }
void Clear(const CPUThreadGuard& guard);
void Save(const CPUThreadGuard& guard, std::FILE* file) const;
@@ -226,19 +234,6 @@ public:
HitPhysicalFalse(this, origin, destination, inst.hex);
}
- // The JIT needs this value, but doesn't need to be a full-on friend.
- static constexpr int GetOffsetOfRecordingActive()
- {
-#ifdef __GNUC__
-#pragma GCC diagnostic push
-#pragma GCC diagnostic ignored "-Winvalid-offsetof"
-#endif
- return offsetof(BranchWatch, m_recording_active);
-#ifdef __GNUC__
-#pragma GCC diagnostic pop
-#endif
- }
-
private:
Collection& GetCollectionV(bool condition)
{
@@ -273,8 +268,4 @@ private:
Collection m_collection_pf; // physical address space | false path
Selection m_selection;
};
-
-#if _M_X86_64
-static_assert(BranchWatch::GetOffsetOfRecordingActive() < 0x80); // Makes JIT code smaller.
-#endif
} // namespace Core
diff --git a/Source/Core/Core/HW/DVD/AMMediaboard.cpp b/Source/Core/Core/HW/DVD/AMMediaboard.cpp
new file mode 100644
index 0000000000..5cd69f8a62
--- /dev/null
+++ b/Source/Core/Core/HW/DVD/AMMediaboard.cpp
@@ -0,0 +1,2180 @@
+// Copyright 2025 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "Core/HW/DVD/AMMediaboard.h"
+
+#include <algorithm>
+#include <bit>
+#include <random>
+#include <string>
+#include <unordered_map>
+
+#include <fmt/format.h>
+
+#include "Common/BitUtils.h"
+#include "Common/CommonTypes.h"
+#include "Common/FileUtil.h"
+#include "Common/IOFile.h"
+#include "Common/Logging/Log.h"
+#include "Common/ScopeGuard.h"
+
+#include "Core/Config/MainSettings.h"
+#include "Core/ConfigManager.h"
+#include "Core/Core.h"
+#include "Core/HLE/HLE.h"
+#include "Core/HW/EXI/EXI_DeviceBaseboard.h"
+#include "Core/HW/Memmap.h"
+#include "Core/IOS/Network/Socket.h"
+#include "Core/Movie.h"
+#include "Core/System.h"
+
+#include "DiscIO/CachedBlob.h"
+
+#if defined(__linux__) or defined(__APPLE__) or defined(__FreeBSD__) or defined(__NetBSD__) or \
+ defined(__HAIKU__)
+
+#include <unistd.h>
+
+#include "Common/UnixUtil.h"
+
+static constexpr auto* closesocket = close;
+static auto ioctlsocket(auto... args)
+{
+ return ioctl(args...);
+}
+
+static constexpr int WSAEWOULDBLOCK = 10035;
+static constexpr int SOCKET_ERROR = -1;
+
+using SOCKET = int;
+using WSAPOLLFD = pollfd;
+
+static constexpr SOCKET INVALID_SOCKET = SOCKET(~0);
+
+static int WSAGetLastError()
+{
+ switch (errno)
+ {
+ case EINPROGRESS:
+ case EWOULDBLOCK:
+ return WSAEWOULDBLOCK;
+ default:
+ break;
+ }
+
+ return errno;
+}
+
+#endif
+
+namespace AMMediaboard
+{
+
+MediaBoardRange::MediaBoardRange(u32 start_, u32 size_, std::span<u8> buffer_)
+ : start{start_}, end{start_ + size_}, buffer{buffer_.data()}, buffer_size{buffer_.size()}
+{
+}
+
+using Common::SEND_FLAGS;
+
+enum class GuestSocket : s32
+{
+};
+static constexpr auto INVALID_GUEST_SOCKET = GuestSocket(-1);
+
+struct TimeVal
+{
+ // TODO: Verify this.
+ u64 seconds;
+ u32 microseconds;
+};
+
+struct GuestFdSet
+{
+ static constexpr std::size_t BIT_COUNT = 256;
+
+ std::array<u8, BIT_COUNT / CHAR_BIT> bits{};
+
+ constexpr bool IsFdSet(GuestSocket s) const
+ {
+ const auto index = std::size_t(s);
+ assert(index < BIT_COUNT);
+ return Common::ExtractBit(bits[index / CHAR_BIT], index % CHAR_BIT) != 0;
+ }
+
+ constexpr void SetFd(GuestSocket s, bool value = true)
+ {
+ const auto index = std::size_t(s);
+ assert(index < BIT_COUNT);
+ Common::SetBit(bits[index / CHAR_BIT], index % CHAR_BIT, value);
+ }
+};
+static_assert(sizeof(GuestFdSet) == 32);
+
+// This seems to be based on VxWorks sockaddr_in.
+struct GuestSocketAddress
+{
+ // Seemingly always zero or random values ? Game bug ?
+ // This is the struct size in VxWorks.
+ u8 unknown_value;
+
+ u8 ip_family;
+ u16 port; // Network byte order.
+ Common::IPAddress ip_address;
+ std::array<u8, 8> padding;
+};
+static_assert(sizeof(GuestSocketAddress) == 16);
+
+static bool s_firmware_map = false;
+static bool s_test_menu = false;
+static std::array<u32, 3> s_timeouts = {20000, 20000, 20000};
+static u32 s_last_error = SSC_SUCCESS;
+
+static u32 s_gcam_key_a = 0;
+static u32 s_gcam_key_b = 0;
+static u32 s_gcam_key_c = 0;
+
+static File::IOFile s_netcfg;
+static File::IOFile s_netctrl;
+static File::IOFile s_extra;
+static File::IOFile s_backup;
+static File::IOFile s_dimm;
+
+static std::unique_ptr<DiscIO::BlobReader> s_dimm_disc;
+
+static std::array<u8, 0x200000> s_firmware;
+static std::array<u32, 0xc0> s_media_buffer_32;
+static u8* const s_media_buffer = reinterpret_cast<u8*>(s_media_buffer_32.data());
+static std::array<u8, 0x4ffe00> s_network_command_buffer;
+static std::array<u8, 0x80000> s_network_buffer;
+static std::array<u8, 0x1000> s_allnet_buffer;
+static std::array<u8, 0x8500> s_allnet_settings;
+
+static Common::IPAddress s_game_modified_ip_address;
+
+// Fake loading the game to have a chance to enter test mode
+static u32 s_board_status = LoadingGameProgram;
+static u32 s_load_progress = 80;
+
+static constexpr std::size_t MAX_IPV4_STRING_LENGTH = 15;
+
+constexpr char s_allnet_reply[] = {
+ "uri=http://"
+ "sega.com&host=sega.com&nickname=sega&name=sega&year=2025&month=08&day=16&hour=21&minute=10&"
+ "second=12&place_id=1234&setting=0x123&region0=jap&region_name0=japan&region_name1=usa&region_"
+ "name2=asia&region_name3=export&end"};
+
+static const MediaBoardRange s_mediaboard_ranges[] = {
+ {DIMMCommandVersion1, 0x40, Common::AsWritableU8Span(s_media_buffer_32)},
+ {DIMMCommandVersion2, 0x60, Common::AsWritableU8Span(s_media_buffer_32)},
+ {DIMMCommandVersion2_2, 0x220, Common::AsWritableU8Span(s_media_buffer_32)},
+ {NetworkCommandAddress1, 0x1040, s_network_command_buffer},
+ {NetworkCommandAddress2, 0x20000, s_network_command_buffer},
+ {NetworkBufferAddress1, 0x10000, s_network_buffer},
+ {NetworkBufferAddress2, 0x10000, s_network_buffer},
+ {NetworkBufferAddress3, 0x50000, s_network_buffer},
+ {NetworkBufferAddress4, 0xc0000, s_network_buffer},
+ {NetworkBufferAddress5, 0x10000, s_network_buffer},
+ {AllNetSettings, 0x8000, s_allnet_settings},
+ {AllNetBuffer, 0x1000, s_allnet_buffer},
+};
+
+static const std::unordered_map<u16, GameType> s_game_map = {
+ {0x4747, FZeroAX},
+ {0x4841, FZeroAXMonster},
+ {0x4B50, MarioKartGP},
+ {0x4B5A, MarioKartGP},
+ {0x4E4A, MarioKartGP2},
+ {0x4E4C, MarioKartGP2},
+ {0x454A, VirtuaStriker3},
+ {0x4559, VirtuaStriker3},
+ {0x4C4A, VirtuaStriker4_2006},
+ {0x4C4B, VirtuaStriker4_2006},
+ {0x4C4C, VirtuaStriker4_2006},
+ {0x484A, VirtuaStriker4},
+ {0x484E, VirtuaStriker4},
+ {0x485A, VirtuaStriker4},
+ {0x4A41, VirtuaStriker4},
+ {0x4A4A, VirtuaStriker4},
+ {0x4658, KeyOfAvalon},
+ {0x4A4E, KeyOfAvalon},
+ {0x4758, GekitouProYakyuu},
+ {0x5342, VirtuaStriker3},
+ {0x3132, VirtuaStriker3},
+ {0x454C, VirtuaStriker3},
+ {0x3030, FirmwareUpdate},
+};
+
+// Sockets FDs are required to go from 0 to 63.
+// Games use the FD as indexes so we have to workaround it.
+
+static std::array<SOCKET, SOCKET_FD_MAX> s_sockets;
+
+// TODO: Verify this.
+static constexpr u32 FIRST_VALID_FD = 1;
+
+static GuestSocket GetAvailableGuestSocket()
+{
+ for (u32 i = FIRST_VALID_FD; i < std::size(s_sockets); ++i)
+ {
+ if (s_sockets[i] == SOCKET_ERROR)
+ return GuestSocket(i);
+ }
+
+ // Out of sockets.
+ return INVALID_GUEST_SOCKET;
+}
+
+static SOCKET GetHostSocket(GuestSocket x)
+{
+ const auto index = u32(x);
+
+ if (index < std::size(s_sockets))
+ return s_sockets[index];
+
+ WARN_LOG_FMT(AMMEDIABOARD, "GC-AM: Bad GuestSocket value: {}", index);
+ return INVALID_SOCKET;
+}
+
+static GuestSocket GetGuestSocket(SOCKET x)
+{
+ const auto it = std::find(std::begin(s_sockets) + FIRST_VALID_FD, std::end(s_sockets), x);
+
+ if (it == std::end(s_sockets))
+ {
+ ERROR_LOG_FMT(AMMEDIABOARD, "GuestSocket not found. This should not happen.");
+ return INVALID_GUEST_SOCKET;
+ }
+
+ return GuestSocket(it - std::begin(s_sockets));
+}
+
+static bool NetworkCMDBufferCheck(u32 offset, u32 length)
+{
+ if (offset <= std::size(s_network_command_buffer) &&
+ length <= std::size(s_network_command_buffer) - offset)
+ {
+ return true;
+ }
+
+ ERROR_LOG_FMT(AMMEDIABOARD, "GC-AM: Invalid command buffer range: offset={}, length={}", offset,
+ length);
+ return false;
+}
+
+static constexpr u8* GetSafePtr(std::span<u8> buffer, u32 buffer_base, u32 offset, u32 length)
+{
+ if (offset >= buffer_base)
+ {
+ const u64 off = offset - buffer_base;
+ if ((off + length) <= buffer.size())
+ return std::data(buffer) + off;
+ }
+
+ if (offset != 0)
+ {
+ ERROR_LOG_FMT(AMMEDIABOARD,
+ "GetSafePtr: buffer[0x{:08x}] base=0x{:08x} offset=0x{:08x} length=0x{:08x}",
+ buffer.size(), buffer_base, offset, length);
+ }
+
+ return nullptr;
+}
+
+static constexpr std::string_view GetSafeString(std::span<u8> buffer, u32 buffer_base, u32 offset,
+ u32 max_length)
+{
+ auto* const ptr = GetSafePtr(buffer, buffer_base, offset, 0);
+
+ if (ptr == nullptr)
+ return {};
+
+ // Don't exceed max_length or end of buffer.
+ const auto adjusted_length =
+ std::min<std::size_t>(max_length, buffer.data() + buffer.size() - ptr);
+ const auto length = strnlen(reinterpret_cast<char*>(ptr), adjusted_length);
+
+ return {reinterpret_cast<char*>(ptr), length};
+}
+
+static bool NetworkBufferCheck(u32 offset, u32 length)
+{
+ if (offset <= std::size(s_network_buffer) && length <= std::size(s_network_buffer) - offset)
+ {
+ return true;
+ }
+
+ ERROR_LOG_FMT(AMMEDIABOARD, "GC-AM: Invalid network buffer range: offset={}, length={}", offset,
+ length);
+ return false;
+}
+
+static bool SafeCopyToEmu(Memory::MemoryManager& memory, u32 address, const u8* source,
+ u64 source_size, u32 offset, u32 length)
+{
+ if (offset > source_size || length > source_size - offset)
+ {
+ ERROR_LOG_FMT(AMMEDIABOARD, "GC-AM: Read overflow: offset=0x{:08x}, length={}, source_size={}",
+ offset, length, source_size);
+ return false;
+ }
+
+ auto span = memory.GetSpanForAddress(address);
+ if (length > span.size())
+ {
+ ERROR_LOG_FMT(AMMEDIABOARD,
+ "GC-AM: Memory buffer too small: address=0x{:08x}, length={}, span={}", address,
+ length, span.size());
+ return false;
+ }
+
+ memory.CopyToEmu(address, source + offset, length);
+ return true;
+}
+
+static bool SafeCopyFromEmu(Memory::MemoryManager& memory, u8* destination, u32 address,
+ u64 destination_size, u32 offset, u32 length)
+{
+ if (offset > destination_size || length > destination_size - offset)
+ {
+ ERROR_LOG_FMT(AMMEDIABOARD,
+ "GC-AM: Write overflow: offset=0x{:08x}, length={}, destination_size={}", offset,
+ length, destination_size);
+ return false;
+ }
+
+ auto span = memory.GetSpanForAddress(address);
+ if (length > span.size())
+ {
+ ERROR_LOG_FMT(AMMEDIABOARD,
+ "GC-AM: Memory buffer too small: address=0x{:08x}, length={}, span={}", address,
+ length, span.size());
+ return false;
+ }
+
+ memory.CopyFromEmu(destination + offset, address, length);
+ return true;
+}
+
+static GuestSocket socket_(int af, int type, int protocol)
+{
+ const auto guest_socket = GetAvailableGuestSocket();
+ if (guest_socket == INVALID_GUEST_SOCKET)
+ return INVALID_GUEST_SOCKET;
+
+ const s32 host_fd = socket(af, type, protocol);
+ if (host_fd < 0)
+ {
+ ERROR_LOG_FMT(AMMEDIABOARD, "GC-AM: failed to create socket ({})", Common::StrNetworkError());
+ return INVALID_GUEST_SOCKET;
+ }
+
+ Common::SetPlatformSocketOptions(host_fd);
+
+ s_sockets[u32(guest_socket)] = host_fd;
+ return guest_socket;
+}
+
+static GuestSocket accept_(int fd, sockaddr* addr, socklen_t* len)
+{
+ const auto guest_socket = GetAvailableGuestSocket();
+ if (guest_socket == INVALID_GUEST_SOCKET)
+ return INVALID_GUEST_SOCKET;
+
+ const s32 host_fd = accept(fd, addr, len);
+ if (host_fd < 0)
+ return INVALID_GUEST_SOCKET;
+
+ Common::SetPlatformSocketOptions(host_fd);
+
+ s_sockets[u32(guest_socket)] = host_fd;
+ return guest_socket;
+}
+
+static inline void PrintMBBuffer(u32 address, u32 length)
+{
+ const auto& system = Core::System::GetInstance();
+ auto& memory = system.GetMemory();
+
+ for (u32 i = 0; i < length; i += 0x10)
+ {
+ DEBUG_LOG_FMT(AMMEDIABOARD, "GC-AM: {:08x} {:08x} {:08x} {:08x}", memory.Read_U32(address + i),
+ memory.Read_U32(address + i + 4), memory.Read_U32(address + i + 8),
+ memory.Read_U32(address + i + 12));
+ }
+}
+
+void FirmwareMap(bool on)
+{
+ s_firmware_map = on;
+}
+
+void InitKeys(u32 key_a, u32 key_b, u32 key_c)
+{
+ s_gcam_key_a = key_a;
+ s_gcam_key_b = key_b;
+ s_gcam_key_c = key_c;
+}
+
+static File::IOFile OpenOrCreateFile(const std::string& filename)
+{
+ // Try opening for read/write first
+ if (File::Exists(filename))
+ return File::IOFile(filename, "rb+");
+
+ // Create new file
+ return File::IOFile(filename, "wb+");
+}
+
+void Init()
+{
+ s_media_buffer_32.fill(0);
+ s_network_buffer.fill(0);
+ s_network_command_buffer.fill(0);
+ s_firmware.fill(-1);
+ s_sockets.fill(SOCKET_ERROR);
+ s_allnet_buffer.fill(0);
+ s_allnet_settings.fill(0);
+
+ s_game_modified_ip_address = {};
+
+ s_board_status = LoadingGameProgram;
+ s_load_progress = 80;
+
+ s_firmware_map = false;
+ s_test_menu = false;
+
+ s_last_error = SSC_SUCCESS;
+
+ s_gcam_key_a = 0;
+ s_gcam_key_b = 0;
+ s_gcam_key_c = 0;
+
+ const std::string base_path = File::GetUserPath(D_TRIUSER_IDX);
+
+ s_netcfg = OpenOrCreateFile(base_path + "trinetcfg.bin");
+ s_netctrl = OpenOrCreateFile(base_path + "trinetctrl.bin");
+ s_extra = OpenOrCreateFile(base_path + "triextra.bin");
+ s_dimm = OpenOrCreateFile(base_path + "tridimm_" + SConfig::GetInstance().GetGameID() + ".bin");
+ s_backup = OpenOrCreateFile(base_path + "backup_" + SConfig::GetInstance().GetGameID() + ".bin");
+
+ if (!s_netcfg.IsOpen())
+ PanicAlertFmt("Failed to open/create: {}", base_path + "trinetcfg.bin");
+ if (!s_netctrl.IsOpen())
+ PanicAlertFmt("Failed to open/create: {}", base_path + "trinetctrl.bin");
+ if (!s_extra.IsOpen())
+ PanicAlertFmt("Failed to open/create: {}", base_path + "triextra.bin");
+ if (!s_dimm.IsOpen())
+ PanicAlertFmt("Failed to open/create: {}", base_path + "tridimm.bin");
+ if (!s_backup.IsOpen())
+ PanicAlertFmt("Failed to open/create: {}", base_path + "backup.bin");
+
+ // This is the firmware for the Triforce
+ const std::string sega_boot_filename = base_path + "segaboot.gcm";
+
+ if (!File::Exists(sega_boot_filename))
+ {
+ PanicAlertFmt("Failed to open segaboot.gcm({}), which is required for test menus.",
+ sega_boot_filename.c_str());
+ return;
+ }
+
+ File::IOFile sega_boot(sega_boot_filename, "rb+");
+ if (!sega_boot.IsOpen())
+ {
+ PanicAlertFmt("Failed to read: {}", sega_boot_filename);
+ return;
+ }
+
+ const u64 length = std::min<u64>(sega_boot.GetSize(), sizeof(s_firmware));
+ sega_boot.ReadBytes(s_firmware.data(), length);
+
+ s_test_menu = true;
+}
+
+void InitDIMM(const DiscIO::Volume& volume)
+{
+ // Load game into RAM, like on the actual Triforce.
+ s_dimm_disc = DiscIO::CreateCachedBlobReader(volume.GetBlobReader().CopyReader());
+}
+
+static int PlatformPoll(std::span<WSAPOLLFD> pfds, std::chrono::milliseconds timeout)
+{
+#if defined(_WIN32)
+ return WSAPoll(pfds.data(), ULONG(pfds.size()), INT(timeout.count()));
+#else
+ return UnixUtil::RetryOnEINTR(poll, pfds.data(), pfds.size(), timeout.count());
+#endif
+}
+
+std::optional<ParsedIPRedirection> ParseIPRedirection(std::string_view str)
+{
+ // Everything after a space is the description.
+ const auto ip_pair_str = std::string_view{str.begin(), std::ranges::find(str, ' ')};
+
+ const auto parts = SplitStringIntoArray<2>(ip_pair_str, '=');
+ if (!parts.has_value())
+ return std::nullopt;
+
+ const bool have_description = ip_pair_str.size() != str.size();
+
+ return ParsedIPRedirection{
+ .original = (*parts)[0],
+ .replacement = (*parts)[1],
+ .description = have_description ? str.substr(ip_pair_str.size() + 1) : std::string_view{},
+ };
+}
+
+// Caller should check if it matches first!
+Common::IPv4Port IPRedirection::Apply(Common::IPv4Port subject) const
+{
+ // This logic could probably be better.
+ // Ranges of different sizes will be weird in general.
+
+ const auto replacement_first_ip_u32 = replacement.first.GetIPAddressValue();
+ const auto ip_count = 1u + u64(replacement.last.GetIPAddressValue()) - replacement_first_ip_u32;
+ const auto result_ip =
+ u32(replacement_first_ip_u32 +
+ ((subject.GetIPAddressValue() - original.first.GetIPAddressValue()) % ip_count));
+
+ subject.ip_address = std::bit_cast<Common::IPAddress>(Common::BigEndianValue{result_ip});
+
+ const auto replacement_first_port_u16 = replacement.first.GetPortValue();
+ const auto port_count = 1u + u32(replacement.last.GetPortValue()) - replacement_first_port_u16;
+
+ // If the replacement includes all ports then we don't alter the port.
+ // This allows "10.0.0.1:80-88=10.0.0.2" to have the expected behavior.
+ if (port_count != 65536u)
+ {
+ const auto result_port_u16 =
+ u16(replacement_first_port_u16 +
+ ((subject.GetPortValue() - original.first.GetPortValue()) % port_count));
+ subject.port = std::bit_cast<u16>(Common::BigEndianValue{result_port_u16});
+ }
+
+ return subject;
+}
+
+Common::IPv4Port IPRedirection::Reverse(Common::IPv4Port subject) const
+{
+ // Low effort implementation..
+ return IPRedirection{.original = replacement, .replacement = original}.Apply(subject);
+}
+
+std::string IPRedirection::ToString() const
+{
+ return fmt::format("{}={}", original.ToString(), replacement.ToString());
+}
+
+IPRedirections GetIPRedirections()
+{
+ IPRedirections result;
+
+ const auto ip_redirections_str = Config::Get(Config::MAIN_TRIFORCE_IP_REDIRECTIONS);
+ for (auto&& ip_pair : ip_redirections_str | std::views::split(','))
+ {
+ const auto ip_pair_str = std::string_view{ip_pair};
+ const auto parts = ParseIPRedirection(ip_pair_str);
+ if (parts.has_value())
+ {
+ const auto original = Common::StringToIPv4PortRange(parts->original);
+ const auto replacement = Common::StringToIPv4PortRange(parts->replacement);
+
+ if (original.has_value() && replacement.has_value())
+ {
+ result.emplace_back(*original, *replacement);
+ continue;
+ }
+
+ ERROR_LOG_FMT(AMMEDIABOARD, "Bad IP redirection string: {}", ip_pair_str);
+ }
+ }
+
+ return result;
+}
+
+static std::optional<Common::IPv4Port> AdjustIPv4PortFromConfig(Common::IPv4Port subject)
+{
+ // TODO: We should parse this elsewhere to avoid repeated string manipulations.
+ for (auto&& redirection : GetIPRedirections())
+ {
+ if (redirection.original.IsMatch(subject))
+ return redirection.Apply(subject);
+ }
+
+ return std::nullopt;
+}
+
+static std::optional<Common::IPv4Port> ReverseAdjustIPv4PortFromConfig(Common::IPv4Port subject)
+{
+ // TODO: We should parse this elsewhere to avoid repeated string manipulations.
+ for (auto&& redirection : GetIPRedirections())
+ {
+ if (redirection.replacement.IsMatch(subject))
+ return redirection.Reverse(subject);
+ }
+
+ return std::nullopt;
+}
+
+// Ports are in host byte order.
+static bool BindEphemeralPort(SOCKET host_socket, Common::IPAddress ip_address,
+ u16 first_port_value, u16 last_port_value, u32 attempt_count)
+{
+ std::mt19937 rng(u32(Clock::now().time_since_epoch().count()));
+ std::uniform_int_distribution<u16> port_distribution{first_port_value, last_port_value};
+
+ sockaddr_in addr = {
+ .sin_family = AF_INET,
+ .sin_addr = std::bit_cast<in_addr>(ip_address),
+ };
+
+ while (attempt_count-- != 0)
+ {
+ const u16 port_value = port_distribution(rng);
+
+ addr.sin_port = htons(port_value);
+
+ const auto bind_result = bind(host_socket, reinterpret_cast<sockaddr*>(&addr), sizeof(addr));
+ const int bind_err = WSAGetLastError();
+
+ INFO_LOG_FMT(AMMEDIABOARD, "BindEphemeralPort: bind ({}:{}) = {} ({})",
+ Common::IPAddressToString(ip_address), port_value, bind_result,
+ Common::DecodeNetworkError(bind_err));
+
+ if (bind_result == 0)
+ return true;
+ }
+
+ return false;
+}
+
+static s32 NetDIMMConnect(GuestSocket guest_socket, const GuestSocketAddress& guest_addr)
+{
+ INFO_LOG_FMT(AMMEDIABOARD, "NetDIMMConnect: {}:{}",
+ Common::IPAddressToString(guest_addr.ip_address), ntohs(guest_addr.port));
+
+ sockaddr_in addr{
+ .sin_family = guest_addr.ip_family,
+ };
+
+ // Adjust destination IP and port.
+ const auto adjusted_ipv4port = AdjustIPv4PortFromConfig({guest_addr.ip_address, guest_addr.port});
+ if (adjusted_ipv4port.has_value())
+ {
+ addr.sin_addr = std::bit_cast<in_addr>(adjusted_ipv4port->ip_address);
+ addr.sin_port = adjusted_ipv4port->port;
+
+ INFO_LOG_FMT(AMMEDIABOARD, "NetDIMMConnect: Redirecting to: {}:{}",
+ Common::IPAddressToString(adjusted_ipv4port->ip_address),
+ ntohs(adjusted_ipv4port->port));
+ }
+ else
+ {
+ addr.sin_addr = std::bit_cast<in_addr>(guest_addr.ip_address);
+ addr.sin_port = guest_addr.port;
+ }
+
+ const auto host_socket = GetHostSocket(guest_socket);
+
+ // See if we have a redirection for the game modified IP.
+ // If so, adjust the source IP by binding the socket.
+ const auto adjusted_source_ipv4port = AdjustIPv4PortFromConfig({s_game_modified_ip_address, 0});
+ if (adjusted_source_ipv4port.has_value())
+ {
+ // FYI: We don't handle the situation if games bind outgoing TCP themselves.
+ // But I think that's unlikely.
+
+ const u16 first_port_value = adjusted_source_ipv4port->GetPortValue();
+
+ // If port zero is included then we don't care about the port number.
+ const bool use_any_port = first_port_value == 0;
+ const u32 attempt_count = use_any_port ? 1 : 10;
+
+ // TODO: Handle the range properly. AdjustIPv4PortFromConfig should return a port range.
+ // This magic 999 is here just to match our default config..
+ const u16 last_port_value = use_any_port ? 0 : first_port_value + 999;
+
+ const auto bind_result = BindEphemeralPort(host_socket, adjusted_source_ipv4port->ip_address,
+ first_port_value, last_port_value, attempt_count);
+
+ if (!bind_result)
+ {
+ s_last_error = SOCKET_ERROR;
+ return SOCKET_ERROR;
+ }
+ }
+
+ // Set socket to non-blocking
+ {
+ u_long val = 1;
+ ioctlsocket(host_socket, FIONBIO, &val);
+ }
+ // Restore blocking mode
+ Common::ScopeGuard guard{[&] {
+ u_long val = 0;
+ ioctlsocket(host_socket, FIONBIO, &val);
+ }};
+
+ const int connect_result =
+ connect(host_socket, reinterpret_cast<const sockaddr*>(&addr), sizeof(addr));
+ const int err = WSAGetLastError();
+
+ INFO_LOG_FMT(AMMEDIABOARD_NET, "NetDIMMConnect: connect( {}({}), ({},{}:{}) ):{} ({})",
+ host_socket, u32(guest_socket), addr.sin_family, inet_ntoa(addr.sin_addr),
+ Common::swap16(addr.sin_port), connect_result, err);
+
+ if (connect_result == 0) [[unlikely]]
+ {
+ // Immediate success.
+ s_last_error = SSC_SUCCESS;
+ return 0;
+ }
+
+ if (err != WSAEWOULDBLOCK)
+ {
+ // Immediate failure (e.g. WSAECONNREFUSED)
+ WARN_LOG_FMT(AMMEDIABOARD, "NetDIMMConnect: connect: {} ({})", err,
+ Common::DecodeNetworkError(err));
+
+ s_last_error = SOCKET_ERROR;
+ return SOCKET_ERROR;
+ }
+
+ WSAPOLLFD pfds[1]{{.fd = host_socket, .events = POLLOUT}};
+
+ const auto timeout =
+ duration_cast<std::chrono::milliseconds>(std::chrono::microseconds{s_timeouts[0]});
+
+ const int poll_result = PlatformPoll(pfds, timeout);
+
+ if (poll_result < 0) [[unlikely]]
+ {
+ // Poll failure.
+ ERROR_LOG_FMT(AMMEDIABOARD, "NetDIMMConnect: PlatformPoll: {}", Common::StrNetworkError());
+
+ s_last_error = SOCKET_ERROR;
+ return SOCKET_ERROR;
+ }
+
+ if ((pfds[0].revents & POLLOUT) == 0)
+ {
+ // Timeout.
+ s_last_error = SSC_EWOULDBLOCK;
+ return SOCKET_ERROR;
+ }
+
+ int so_error = 0;
+ socklen_t optlen = sizeof(so_error);
+ const int getsockopt_result =
+ getsockopt(host_socket, SOL_SOCKET, SO_ERROR, reinterpret_cast<char*>(&so_error), &optlen);
+
+ if (getsockopt_result != 0) [[unlikely]]
+ {
+ // getsockopt failure.
+ ERROR_LOG_FMT(AMMEDIABOARD, "NetDIMMConnect: getsockopt: {}", Common::StrNetworkError());
+ }
+ else if (so_error == 0)
+ {
+ s_last_error = SSC_SUCCESS;
+ return 0;
+ }
+
+ s_last_error = SOCKET_ERROR;
+ return SOCKET_ERROR;
+}
+
+static GuestSocket NetDIMMAccept(GuestSocket guest_socket, u8* guest_addr_ptr,
+ u8* guest_addrlen_ptr)
+{
+ // Either both parameters should be provided, or neither.
+ if ((guest_addr_ptr != nullptr) != (guest_addrlen_ptr != nullptr))
+ {
+ ERROR_LOG_FMT(AMMEDIABOARD_NET, "NetDIMMAccept: bad parmeters");
+
+ // TODO: Not hardware tested.
+ s_last_error = SSC_EFAULT;
+ return INVALID_GUEST_SOCKET;
+ }
+
+ const auto host_socket = GetHostSocket(guest_socket);
+ WSAPOLLFD pfds[1]{{.fd = host_socket, .events = POLLIN}};
+
+ // FYI: Currently using a 0ms timeout to make accept calls always non-blocking.
+ constexpr auto timeout = std::chrono::milliseconds{0};
+
+ DEBUG_LOG_FMT(AMMEDIABOARD, "NetDIMMAccept: {}({})", host_socket, int(guest_socket));
+
+ const int poll_result = PlatformPoll(pfds, timeout);
+
+ if (poll_result < 0) [[unlikely]]
+ {
+ // Poll failure.
+ ERROR_LOG_FMT(AMMEDIABOARD, "NetDIMMAccept: PlatformPoll: {}", Common::StrNetworkError());
+
+ s_last_error = SOCKET_ERROR;
+ return INVALID_GUEST_SOCKET;
+ }
+
+ if ((pfds[0].revents & POLLIN) == 0)
+ {
+ // Timeout.
+ DEBUG_LOG_FMT(AMMEDIABOARD, "NetDIMMAccept: Timeout.");
+
+ s_last_error = SSC_EWOULDBLOCK;
+ return INVALID_GUEST_SOCKET;
+ }
+
+ sockaddr_in addr;
+ socklen_t addrlen = sizeof(addr);
+ const auto client_sock = accept_(host_socket, reinterpret_cast<sockaddr*>(&addr), &addrlen);
+
+ if (client_sock == INVALID_GUEST_SOCKET)
+ {
+ ERROR_LOG_FMT(AMMEDIABOARD, "AMMBCommandAccept: accept: ({})", Common::StrNetworkError());
+ s_last_error = SOCKET_ERROR;
+ return INVALID_GUEST_SOCKET;
+ }
+
+ s_last_error = SSC_SUCCESS;
+
+ NOTICE_LOG_FMT(AMMEDIABOARD, "AMMBCommandAccept: {}:{}",
+ Common::IPAddressToString(std::bit_cast<Common::IPAddress>(addr.sin_addr)),
+ ntohs(addr.sin_port));
+
+ if (guest_addr_ptr == nullptr)
+ return client_sock;
+
+ GuestSocketAddress guest_addr{
+ .ip_family = u8(addr.sin_family),
+ .port = addr.sin_port,
+ .ip_address = std::bit_cast<Common::IPAddress>(addr.sin_addr),
+ };
+
+ if (const auto adjusted_ipv4port =
+ ReverseAdjustIPv4PortFromConfig({guest_addr.ip_address, guest_addr.port}))
+ {
+ guest_addr.ip_address = adjusted_ipv4port->ip_address;
+ guest_addr.port = adjusted_ipv4port->port;
+
+ NOTICE_LOG_FMT(AMMEDIABOARD, "AMMBCommandAccept: Translating result to: {}:{}",
+ Common::IPAddressToString(guest_addr.ip_address), ntohs(guest_addr.port));
+ }
+
+ const auto write_size =
+ std::min<u32>(Common::BitCastPtr<u32>(guest_addrlen_ptr), sizeof(guest_addr));
+
+ // Write out the addr.
+ std::memcpy(guest_addr_ptr, &guest_addr, write_size);
+
+ // Write out the addrlen.
+ *guest_addrlen_ptr = sizeof(guest_addr);
+
+ return client_sock;
+}
+
+static Common::IPv4Port GetAdjustedBindIPv4Port(Common::IPv4Port socket_addr)
+{
+ auto considered_ipv4 = socket_addr;
+
+ if (std::bit_cast<u32>(considered_ipv4.ip_address) == INADDR_ANY)
+ {
+ // Because the game is binding to "0.0.0.0",
+ // use the "game modified" IP for redirection purposes.
+ // If no redirection applies, then we still bind "0.0.0.0".
+ considered_ipv4.ip_address = s_game_modified_ip_address;
+ INFO_LOG_FMT(AMMEDIABOARD, "GetAdjustedBindIPv4Port: Considering game modified IP: {}",
+ Common::IPAddressToString(s_game_modified_ip_address));
+ }
+
+ if (const auto adjusted_ipv4 = AdjustIPv4PortFromConfig(considered_ipv4))
+ {
+ socket_addr = *adjusted_ipv4;
+ INFO_LOG_FMT(AMMEDIABOARD, "GetAdjustedBindIPv4Port: Redirecting to: {}:{}",
+ Common::IPAddressToString(socket_addr.ip_address), ntohs(socket_addr.port));
+ }
+
+ return socket_addr;
+}
+
+static u32 NetDIMMBind(GuestSocket guest_socket, const GuestSocketAddress& guest_addr)
+{
+ const auto host_socket = GetHostSocket(guest_socket);
+
+ NOTICE_LOG_FMT(AMMEDIABOARD, "NetDIMMBind: {}({}) {}, {}, {}:{}", host_socket, int(guest_socket),
+ guest_addr.unknown_value, guest_addr.ip_family,
+ Common::IPAddressToString(guest_addr.ip_address), ntohs(guest_addr.port));
+
+ const auto adjusted_ipv4port = GetAdjustedBindIPv4Port({guest_addr.ip_address, guest_addr.port});
+
+ sockaddr_in addr{
+ .sin_family = guest_addr.ip_family,
+ .sin_port = adjusted_ipv4port.port,
+ .sin_addr = std::bit_cast<in_addr>(adjusted_ipv4port.ip_address),
+ };
+
+ const int bind_result = bind(host_socket, reinterpret_cast<const sockaddr*>(&addr), sizeof(addr));
+ const int err = WSAGetLastError();
+
+ INFO_LOG_FMT(AMMEDIABOARD_NET, "NetDIMMBind: bind( {}({}), ({},{}:{}) ):{}", host_socket,
+ u32(guest_socket), addr.sin_family,
+ Common::IPAddressToString(adjusted_ipv4port.ip_address),
+ Common::swap16(adjusted_ipv4port.port), bind_result);
+
+ if (bind_result < 0)
+ {
+ const auto* const err_msg = Common::DecodeNetworkError(err);
+ ERROR_LOG_FMT(AMMEDIABOARD, "NetDIMMBind bind() = {} ({})", err, err_msg);
+
+ PanicAlertFmt("Failed to bind socket {}:{}\nError: {} ({})",
+ Common::IPAddressToString(adjusted_ipv4port.ip_address),
+ ntohs(adjusted_ipv4port.port), err, err_msg);
+ }
+
+ return bind_result;
+}
+
+static void AMMBCommandRecv(u32 parameter_offset, u32 network_buffer_base)
+{
+ const auto fd = GetHostSocket(GuestSocket(s_media_buffer_32[parameter_offset]));
+ u32 off = s_media_buffer_32[parameter_offset + 1];
+ auto len = std::min<u32>(s_media_buffer_32[parameter_offset + 2], sizeof(s_network_buffer));
+ const u64 off_len = u64(off) + len;
+
+ if (off >= network_buffer_base && off_len <= network_buffer_base + sizeof(s_network_buffer))
+ {
+ off -= network_buffer_base;
+ }
+ else if (off_len > sizeof(s_network_buffer))
+ {
+ ERROR_LOG_FMT(AMMEDIABOARD_NET,
+ "GC-AM: recv(error) invalid destination or length: off={:08x}, len={}", off, len);
+ off = 0;
+ len = 0;
+ }
+
+ const int ret = recv(fd, reinterpret_cast<char*>(s_network_buffer.data() + off), len, 0);
+ const int err = WSAGetLastError();
+
+ DEBUG_LOG_FMT(AMMEDIABOARD_NET, "GC-AM: recv( {}, 0x{:08x}, {} ):{} {}", fd, off, len, ret, err);
+
+ s_media_buffer[1] = s_media_buffer[8];
+ s_media_buffer_32[1] = ret;
+}
+
+static void AMMBCommandSend(u32 parameter_offset, u32 network_buffer_base)
+{
+ const auto guest_socket = GuestSocket(s_media_buffer_32[parameter_offset]);
+ const auto fd = GetHostSocket(guest_socket);
+ u32 off = s_media_buffer_32[parameter_offset + 1];
+ auto len = std::min<u32>(s_media_buffer_32[parameter_offset + 2], sizeof(s_network_buffer));
+ const u64 off_len = u64(off) + len;
+
+ if (off >= network_buffer_base && off_len <= network_buffer_base + sizeof(s_network_buffer))
+ {
+ off -= network_buffer_base;
+ }
+ else if (off_len > sizeof(s_network_buffer))
+ {
+ ERROR_LOG_FMT(AMMEDIABOARD_NET,
+ "GC-AM: send(error) unhandled destination or length: {:08x}, len={}", off, len);
+ off = 0;
+ len = 0;
+ }
+
+ const int ret = send(fd, reinterpret_cast<char*>(s_network_buffer.data() + off), len, SEND_FLAGS);
+ const int err = WSAGetLastError();
+
+ DEBUG_LOG_FMT(AMMEDIABOARD_NET, "GC-AM: send( {}({}), 0x{:08x}, {} ): {} {}", fd,
+ u32(guest_socket), off, len, ret, err);
+
+ s_media_buffer[1] = s_media_buffer[8];
+ s_media_buffer_32[1] = ret;
+}
+
+static void AMMBCommandSocket(u32 parameter_offset)
+{
+ // Protocol is not sent (determined automatically).
+ const u32 domain = s_media_buffer_32[parameter_offset];
+ const u32 type = s_media_buffer_32[parameter_offset + 1];
+
+ const GuestSocket guest_socket = socket_(int(domain), int(type), 0);
+
+ NOTICE_LOG_FMT(AMMEDIABOARD_NET, "GC-AM: socket( {}, {} ):{}", domain, type, u32(guest_socket));
+
+ s_media_buffer[1] = 0;
+ s_media_buffer_32[1] = u32(guest_socket);
+}
+
+static void AMMBCommandClosesocket(u32 parameter_offset)
+{
+ const auto guest_socket = GuestSocket(s_media_buffer_32[parameter_offset]);
+ const auto fd = GetHostSocket(guest_socket);
+
+ const int ret = closesocket(fd);
+
+ NOTICE_LOG_FMT(AMMEDIABOARD_NET, "GC-AM: closesocket( {}({}) ):{}", fd, u32(guest_socket), ret);
+
+ if (u32(guest_socket) < std::size(s_sockets))
+ s_sockets[u32(guest_socket)] = SOCKET_ERROR;
+
+ s_media_buffer_32[1] = ret;
+ s_last_error = SSC_SUCCESS;
+}
+
+static void AMMBCommandConnect(u32 parameter_offset, u32 network_buffer_base)
+{
+ const auto guest_socket = GuestSocket(s_media_buffer_32[parameter_offset + 0]);
+ const u32 addr_offset = s_media_buffer_32[parameter_offset + 1];
+ const u32 len = s_media_buffer_32[parameter_offset + 2];
+
+ GuestSocketAddress addr;
+
+ if (len != sizeof(addr))
+ {
+ ERROR_LOG_FMT(AMMEDIABOARD_NET, "AMMBCommandConnect: Unexpected length: {}", len);
+ return;
+ }
+
+ const auto* addr_ptr =
+ GetSafePtr(s_network_command_buffer, network_buffer_base, addr_offset, sizeof(addr));
+ if (addr_ptr == nullptr)
+ return;
+
+ memcpy(&addr, addr_ptr, sizeof(addr));
+
+ const int ret = NetDIMMConnect(guest_socket, addr);
+
+ s_media_buffer[1] = s_media_buffer[8];
+ s_media_buffer_32[1] = ret;
+}
+
+static void AMMBCommandAccept(u32 parameter_offset, u32 network_buffer_base)
+{
+ const auto guest_socket = GuestSocket(s_media_buffer_32[parameter_offset]);
+ const u32 addr_off = s_media_buffer_32[parameter_offset + 1];
+ const u32 addrlen_off = s_media_buffer_32[parameter_offset + 2];
+
+ auto* const addrlen_ptr =
+ GetSafePtr(s_network_command_buffer, network_buffer_base, addrlen_off, sizeof(u32));
+
+ auto* const addr_ptr = (addrlen_ptr == nullptr) ?
+ nullptr :
+ GetSafePtr(s_network_command_buffer, network_buffer_base, addr_off,
+ Common::BitCastPtr<u32>(addrlen_ptr));
+
+ const auto accept_result = NetDIMMAccept(guest_socket, addr_ptr, addrlen_ptr);
+
+ s_media_buffer_32[1] = u32(accept_result);
+}
+
+static void AMMBCommandBind()
+{
+ const auto guest_socket = GuestSocket(s_media_buffer_32[2]);
+ const u32 addr_offset = s_media_buffer_32[3];
+ const u32 len = s_media_buffer_32[4];
+
+ GuestSocketAddress guest_addr;
+
+ if (len != sizeof(guest_addr))
+ {
+ ERROR_LOG_FMT(AMMEDIABOARD_NET, "AMMBCommandBind: Unexpected length: {}", len);
+ return;
+ }
+
+ const auto* addr_ptr =
+ GetSafePtr(s_network_command_buffer, NetworkCommandAddress2, addr_offset, sizeof(guest_addr));
+ if (addr_ptr == nullptr)
+ return;
+
+ memcpy(&guest_addr, addr_ptr, sizeof(guest_addr));
+
+ const auto bind_result = NetDIMMBind(guest_socket, guest_addr);
+
+ s_media_buffer_32[1] = bind_result;
+ s_last_error = SSC_SUCCESS;
+}
+
+// Expects a pointer to a GuestFdSet or nullptr.
+static void FillPollFdsFromGuestFdSet(std::span<WSAPOLLFD> pfds, const void* guest_fds_ptr,
+ short requested_events)
+{
+ if (guest_fds_ptr == nullptr)
+ return;
+
+ GuestFdSet guest_fds;
+ std::memcpy(&guest_fds, guest_fds_ptr, sizeof(guest_fds));
+
+ u32 index = 0;
+ for (auto& pfd : pfds)
+ {
+ const auto guest_socket = GuestSocket(index);
+ if (guest_fds.IsFdSet(guest_socket))
+ {
+ pfd.fd = GetHostSocket(guest_socket);
+ pfd.events |= requested_events;
+ }
+
+ ++index;
+ }
+}
+
+// Expects a pointer to a GuestFdSet or nullptr.
+static void WriteGuestFdSetFromPollFds(void* guest_fds_ptr, std::span<const WSAPOLLFD> fds,
+ short returned_events)
+{
+ if (guest_fds_ptr == nullptr)
+ return;
+
+ GuestFdSet guest_fds;
+
+ for (const auto& fd : fds)
+ {
+ if ((fd.revents & returned_events) == 0)
+ continue;
+
+ const auto guest_socket = GetGuestSocket(fd.fd);
+ if (guest_socket == INVALID_GUEST_SOCKET)
+ continue;
+
+ guest_fds.SetFd(guest_socket);
+ }
+
+ std::memcpy(guest_fds_ptr, &guest_fds, sizeof(guest_fds));
+}
+
+static void AMMBCommandSelect(u32 parameter_offset, u32 network_buffer_base)
+{
+ u32 nfds = int(s_media_buffer_32[parameter_offset]);
+
+ const u32 readfds_offset = s_media_buffer_32[parameter_offset + 1];
+ auto* const guest_readfds_ptr =
+ GetSafePtr(s_network_command_buffer, network_buffer_base, readfds_offset, sizeof(GuestFdSet));
+
+ const u32 writefds_offset = s_media_buffer_32[parameter_offset + 2];
+ auto* const guest_writefds_ptr = GetSafePtr(s_network_command_buffer, network_buffer_base,
+ writefds_offset, sizeof(GuestFdSet));
+
+ const u32 exceptfds_offset = s_media_buffer_32[parameter_offset + 3];
+ auto* const guest_exceptfds_ptr = GetSafePtr(s_network_command_buffer, network_buffer_base,
+ exceptfds_offset, sizeof(GuestFdSet));
+
+ const u32 timeout_offset = s_media_buffer_32[parameter_offset + 4];
+ auto* const guest_timeout_ptr =
+ GetSafePtr(s_network_command_buffer, network_buffer_base, timeout_offset, sizeof(TimeVal));
+
+ // Games sometimes send 256 (the bit size of GuestFdSet).
+ nfds = std::min<u32>(nfds, std::size(s_sockets));
+
+ std::chrono::milliseconds timeout{-1};
+ if (guest_timeout_ptr != nullptr)
+ {
+ TimeVal guest_timeout;
+ std::memcpy(&guest_timeout, guest_timeout_ptr, sizeof(guest_timeout));
+
+ timeout = duration_cast<std::chrono::milliseconds>(
+ std::chrono::seconds(guest_timeout.seconds) +
+ std::chrono::microseconds(guest_timeout.microseconds));
+ }
+
+ if (timeout < std::chrono::milliseconds{})
+ {
+ // TODO: We should have a way to break out of any timeout on shutdown.
+ // e.g. include a "wakeup" socket in each `poll` call.
+ WARN_LOG_FMT(AMMEDIABOARD, "AMMBCommandSelect: Infinite timout!");
+ }
+
+ DEBUG_LOG_FMT(AMMEDIABOARD_NET,
+ "GC-AM: select( {}, 0x{:08x} 0x{:08x} 0x{:08x} 0x{:08x} ) timeout={}", nfds,
+ readfds_offset, writefds_offset, exceptfds_offset, timeout_offset, timeout.count());
+
+ // Fill with the host sockets for each guest socket less-than `nfds` in each GuestFdSet.
+ std::vector<WSAPOLLFD> pollfds(nfds, WSAPOLLFD{.fd = INVALID_SOCKET});
+
+ FillPollFdsFromGuestFdSet(pollfds, guest_readfds_ptr, POLLIN);
+ FillPollFdsFromGuestFdSet(pollfds, guest_writefds_ptr, POLLOUT);
+ FillPollFdsFromGuestFdSet(pollfds, guest_exceptfds_ptr, POLLPRI);
+
+ // Erase "INVALID" entries and also entries that weren't in any GuestFdSet.
+ std::erase_if(pollfds, [](const WSAPOLLFD& fd) { return fd.fd == INVALID_SOCKET; });
+
+ // TODO: There may be some edge cases where
+ // poll's (POLLIN,POLLOUT,POLLPRI) don't map 1:1 with select's (readfds,writefds,exceptfds).
+
+ DEBUG_LOG_FMT(AMMEDIABOARD, "AMMBCommandSelect: Polling with socket count: {}", pollfds.size());
+
+ const int ret = PlatformPoll(pollfds, timeout);
+
+ if (ret >= 0)
+ {
+ WriteGuestFdSetFromPollFds(guest_readfds_ptr, pollfds, POLLIN);
+ WriteGuestFdSetFromPollFds(guest_writefds_ptr, pollfds, POLLOUT);
+ WriteGuestFdSetFromPollFds(guest_exceptfds_ptr, pollfds, POLLPRI);
+ }
+
+ DEBUG_LOG_FMT(AMMEDIABOARD_NET, "GC-AM: select result: {}", ret);
+
+ s_media_buffer[1] = 0;
+ s_media_buffer_32[1] = ret;
+}
+
+static void AMMBCommandSetSockOpt(u32 parameter_offset, u32 network_buffer_base)
+{
+ const auto fd = GetHostSocket(GuestSocket(s_media_buffer_32[parameter_offset]));
+ const int level = static_cast<int>(s_media_buffer_32[parameter_offset + 1]);
+ const int optname = static_cast<int>(s_media_buffer_32[parameter_offset + 2]);
+ const u32 optval_offset = s_media_buffer_32[parameter_offset + 3] - network_buffer_base;
+ const int optlen = static_cast<int>(s_media_buffer_32[parameter_offset + 4]);
+
+ if (!NetworkCMDBufferCheck(optval_offset, optlen))
+ {
+ return;
+ }
+
+ const char* optval = reinterpret_cast<char*>(s_network_command_buffer.data() + optval_offset);
+
+ // TODO: Ensure parameters are compatible with host's setsockopt
+ const int ret = setsockopt(fd, level, optname, optval, optlen);
+ const int err = WSAGetLastError();
+
+ NOTICE_LOG_FMT(AMMEDIABOARD_NET, "GC-AM: setsockopt( {:d}, {:04x}, {}, {:p}, {} ):{:d} ({})", fd,
+ level, optname, optval, optlen, ret, err);
+
+ s_media_buffer[1] = s_media_buffer[8];
+ s_media_buffer_32[1] = ret;
+}
+
+static void AMMBCommandModifyMyIPaddr(u32 parameter_offset, u32 network_buffer_base)
+{
+ const u32 ip_address_offset = s_media_buffer_32[parameter_offset];
+ const auto ip_address_str = GetSafeString(s_network_command_buffer, network_buffer_base,
+ ip_address_offset, MAX_IPV4_STRING_LENGTH);
+
+ NOTICE_LOG_FMT(AMMEDIABOARD_NET, "GC-AM: modifyMyIPaddr({})", ip_address_str);
+
+ if (const auto parse_result = Common::StringToIPv4PortRange(ip_address_str))
+ s_game_modified_ip_address = parse_result->first.ip_address;
+}
+
+static void FileWriteData(Memory::MemoryManager& memory, File::IOFile* file, u32 seek_pos,
+ u32 address, std::size_t length)
+{
+ auto span = memory.GetSpanForAddress(address);
+ if (length <= span.size())
+ {
+ file->Seek(seek_pos, File::SeekOrigin::Begin);
+ file->WriteBytes(span.data(), length);
+ file->Flush();
+ }
+ else
+ {
+ ERROR_LOG_FMT(AMMEDIABOARD, "GC-AM: Write overflow: address=0x{:08x}, length={}, span={}",
+ address, length, span.size());
+ }
+}
+
+static void FileReadData(Memory::MemoryManager& memory, File::IOFile* file, u32 seek_pos,
+ u32 address, std::size_t length)
+{
+ auto span = memory.GetSpanForAddress(address);
+ if (length <= span.size())
+ {
+ file->Seek(seek_pos, File::SeekOrigin::Begin);
+ file->ReadBytes(span.data(), length);
+ }
+ else
+ {
+ ERROR_LOG_FMT(AMMEDIABOARD, "GC-AM: Read overflow: address=0x{:08x}, length={}, span={}",
+ address, length, span.size());
+ }
+}
+
+u32 ExecuteCommand(std::array<u32, 3>& dicmd_buf, u32* diimm_buf, u32 address, u32 length)
+{
+ auto& system = Core::System::GetInstance();
+ auto& memory = system.GetMemory();
+
+ dicmd_buf[0] ^= s_gcam_key_a;
+ dicmd_buf[1] ^= s_gcam_key_b;
+ dicmd_buf[2] ^= s_gcam_key_c;
+
+ const u32 seed = dicmd_buf[0] >> 16;
+
+ s_gcam_key_a *= seed;
+ s_gcam_key_b *= seed;
+ s_gcam_key_c *= seed;
+
+ // Key setup for Triforce IPL:
+ // These RAM offsets always hold the key for the next command. Since two dummy commands are sent
+ // before any real ones, you can just use the key from RAM without missing a real command.
+
+ if (s_gcam_key_a == 0)
+ {
+ if (memory.Read_U32(0))
+ {
+ HLE::Patch(system, 0x813048B8, "OSReport");
+ HLE::Patch(system, 0x8130095C, "OSReport"); // Apploader
+
+ InitKeys(memory.Read_U32(0), memory.Read_U32(4), memory.Read_U32(8));
+ }
+ }
+
+ const u32 command = dicmd_buf[0] << 24;
+ const u32 offset = dicmd_buf[1] << 2;
+
+ DEBUG_LOG_FMT(AMMEDIABOARD,
+ "GC-AM: {:08x} {:08x} DMA=addr:{:08x},len:{:08x} Keys: {:08x} {:08x} {:08x}",
+ command, offset, address, length, s_gcam_key_a, s_gcam_key_b, s_gcam_key_c);
+
+ // Test mode
+ if (offset == 0x0002440)
+ {
+ // Set by OSResetSystem
+ if (memory.Read_U32(0x811FFF00) == 1)
+ {
+ // Don't map firmware while in SegaBoot
+ if (memory.Read_U32(0x8006BF70) != 0x0A536567)
+ {
+ s_firmware_map = true;
+ }
+ }
+ }
+
+ switch (AMMBDICommand(command >> 24))
+ {
+ case AMMBDICommand::Inquiry:
+ if (s_firmware_map)
+ {
+ s_firmware_map = false;
+ }
+
+ // Returned value is used to set the protocol version.
+ switch (GetGameType())
+ {
+ default:
+ *diimm_buf = Version1;
+ return 0;
+ case VirtuaStriker4_2006:
+ case KeyOfAvalon:
+ case MarioKartGP:
+ case MarioKartGP2:
+ case FirmwareUpdate:
+ *diimm_buf = Version2;
+ return 0;
+ }
+ break;
+ case AMMBDICommand::Read:
+ if ((offset & 0x8FFF0000) == 0x80000000)
+ {
+ switch (offset)
+ {
+ case MediaBoardStatus1:
+ memory.Write_U16(1, address);
+ break;
+ case MediaBoardStatus2:
+ memory.Memset(address, 0, length);
+ break;
+ case MediaBoardStatus3:
+ memory.Memset(address, 0xFF, length);
+ // DIMM size (512MB)
+ memory.Write_U32_Swap(0x20000000, address);
+ // GCAM signature
+ memory.Write_U32(0x4743414D, address + 4);
+ break;
+ case 0x80000100:
+ memory.Write_U32_Swap(0x001F1F1F, address);
+ break;
+ case FirmwareStatus1:
+ memory.Write_U32_Swap(0x01FA, address);
+ break;
+ case FirmwareStatus2:
+ memory.Write_U32_Swap(1, address);
+ break;
+ case 0x80000160:
+ memory.Write_U32(0x00001E00, address);
+ break;
+ case 0x80000180:
+ memory.Write_U32(0, address);
+ break;
+ case 0x800001A0:
+ memory.Write_U32(0xFFFFFFFF, address);
+ break;
+ default:
+ PrintMBBuffer(address, length);
+ PanicAlertFmtT("Unhandled Media Board Read: offset={0:08x} length={0:08x}", offset, length);
+ break;
+ }
+ return 0;
+ }
+
+ // Network configuration
+ if (offset == 0x00000000 && length == 0x80)
+ {
+ FileReadData(memory, &s_netcfg, 0, address, length);
+ return 0;
+ }
+
+ // Media crc check on/off
+ if (offset == DIMMExtraSettings && length == 0x20)
+ {
+ FileReadData(memory, &s_extra, 0, address, length);
+ return 0;
+ }
+
+ // DIMM memory (8MB)
+ if (offset >= DIMMMemory && offset < 0x1F800000)
+ {
+ FileReadData(memory, &s_dimm, offset - DIMMMemory, address, length);
+ return 0;
+ }
+
+ // DIMM memory (8MB)
+ if (offset >= DIMMMemory2 && offset < 0xFF800000)
+ {
+ FileReadData(memory, &s_dimm, offset - DIMMMemory2, address, length);
+ return 0;
+ }
+
+ if (offset == NetworkControl && length == 0x20)
+ {
+ FileReadData(memory, &s_netctrl, 0, address, length);
+ return 0;
+ }
+
+ if (offset >= AllNetBuffer && offset < 0x89011000)
+ {
+ INFO_LOG_FMT(AMMEDIABOARD, "GC-AM: Read All.Net Buffer ({:08x},{})", offset, length);
+ // Fake reply
+ SafeCopyToEmu(memory, address, reinterpret_cast<const u8*>(s_allnet_reply),
+ sizeof(s_allnet_reply), offset - AllNetBuffer, sizeof(s_allnet_reply));
+ return 0;
+ }
+
+ for (const auto& range : s_mediaboard_ranges)
+ {
+ if (offset >= range.start && offset < range.end)
+ {
+ DEBUG_LOG_FMT(AMMEDIABOARD, "GC-AM: Read MediaBoard ({:08x},{:08x},{:08x})", offset,
+ range.start, length);
+ SafeCopyToEmu(memory, address, range.buffer, range.buffer_size, offset - range.start,
+ length);
+ PrintMBBuffer(address, length);
+ return 0;
+ }
+ }
+
+ if (offset == DIMMCommandExecute2)
+ {
+ const AMMBCommand ammb_command = Common::BitCastPtr<AMMBCommand>(s_media_buffer + 0x202);
+
+ DEBUG_LOG_FMT(AMMEDIABOARD, "GC-AM: Execute command: (2){0:04X}",
+ static_cast<u16>(ammb_command));
+
+ memcpy(s_media_buffer, s_media_buffer + 0x200, 0x20);
+ memset(s_media_buffer + 0x200, 0, 0x20);
+ s_media_buffer[0x204] = 1;
+
+ switch (ammb_command)
+ {
+ case AMMBCommand::Unknown_001:
+ s_media_buffer_32[1] = 1;
+ break;
+ case AMMBCommand::GetNetworkFirmVersion:
+ s_media_buffer_32[1] = 0x1305; // Version: 13.05
+ s_media_buffer[6] = 1; // Type: VxWorks
+ break;
+ case AMMBCommand::GetSystemFlags:
+ s_media_buffer[4] = 1;
+ s_media_buffer[6] = NANDMaskBoardNAND;
+ s_media_buffer[7] = 1;
+ break;
+ // Empty reply
+ case AMMBCommand::Unknown_103:
+ break;
+ case AMMBCommand::Unknown_104:
+ s_media_buffer[4] = 1;
+ break;
+ case AMMBCommand::Accept:
+ AMMBCommandAccept(2, NetworkCommandAddress2);
+ break;
+ case AMMBCommand::Bind:
+ AMMBCommandBind();
+ break;
+ case AMMBCommand::Closesocket:
+ AMMBCommandClosesocket(2);
+ break;
+ case AMMBCommand::Connect:
+ AMMBCommandConnect(2, NetworkCommandAddress2);
+ break;
+ case AMMBCommand::InetAddr:
+ {
+ const char* ip_address = reinterpret_cast<char*>(s_network_command_buffer.data());
+
+ // IP address shouldn't be longer than 15
+ // TODO: Shouldn't this look at 16 characters for lack of null-termination?
+ if (strnlen(ip_address, MAX_IPV4_STRING_LENGTH) > MAX_IPV4_STRING_LENGTH)
+ {
+ ERROR_LOG_FMT(AMMEDIABOARD_NET, "GC-AM: Invalid size for address: InetAddr():{}",
+ strlen(ip_address));
+ break;
+ }
+
+ const u32 ip = inet_addr(ip_address);
+ NOTICE_LOG_FMT(AMMEDIABOARD_NET, "GC-AM: InetAddr( {} )", ip_address);
+
+ s_media_buffer[1] = s_media_buffer[8];
+ s_media_buffer_32[1] = ip;
+ break;
+ }
+ case AMMBCommand::Listen:
+ {
+ const auto fd = GetHostSocket(GuestSocket(s_media_buffer_32[2]));
+ const u32 backlog = s_media_buffer_32[3];
+
+ const int ret = listen(fd, backlog);
+
+ NOTICE_LOG_FMT(AMMEDIABOARD_NET, "GC-AM: listen( {}, {} ):{:d}", fd, backlog, ret);
+
+ s_media_buffer[1] = s_media_buffer[8];
+ s_media_buffer_32[1] = ret;
+ break;
+ }
+ case AMMBCommand::Recv:
+ AMMBCommandRecv(2, NetworkBufferAddress4);
+ break;
+ case AMMBCommand::Send:
+ AMMBCommandSend(2, NetworkBufferAddress3);
+ break;
+ case AMMBCommand::Socket:
+ AMMBCommandSocket(2);
+ break;
+ case AMMBCommand::Select:
+ AMMBCommandSelect(2, NetworkCommandAddress2);
+ break;
+ case AMMBCommand::SetSockOpt:
+ AMMBCommandSetSockOpt(2, NetworkCommandAddress2);
+ break;
+ case AMMBCommand::SetTimeOuts:
+ {
+ const auto guest_socket = GuestSocket(s_media_buffer_32[2]);
+ const auto host_socket = GetHostSocket(guest_socket);
+ const u32 timeout_a = s_media_buffer_32[3];
+ const u32 timeout_b = s_media_buffer_32[4];
+ const u32 timeout_c = s_media_buffer_32[5];
+
+ s_timeouts[0] = timeout_a;
+ s_timeouts[1] = timeout_b;
+ s_timeouts[2] = timeout_c;
+
+ int ret = SOCKET_ERROR;
+
+ if (host_socket != INVALID_SOCKET)
+ {
+ ret = setsockopt(host_socket, SOL_SOCKET, SO_SNDTIMEO,
+ reinterpret_cast<const char*>(&timeout_b), sizeof(int));
+ if (ret < 0)
+ {
+ ret = WSAGetLastError();
+ }
+ else
+ {
+ ret = setsockopt(host_socket, SOL_SOCKET, SO_RCVTIMEO,
+ reinterpret_cast<const char*>(&timeout_c), sizeof(int));
+ if (ret < 0)
+ ret = WSAGetLastError();
+ }
+
+ INFO_LOG_FMT(AMMEDIABOARD_NET, "GC-AM: SetTimeOuts( {}({}), {}, {}, {} ):{}", host_socket,
+ int(guest_socket), timeout_a, timeout_b, timeout_c, ret);
+ }
+ else
+ {
+ ERROR_LOG_FMT(AMMEDIABOARD_NET,
+ "GC-AM: Invalid Socket: SetTimeOuts( {}({}), {}, {}, {} ):{}", host_socket,
+ int(guest_socket), timeout_a, timeout_b, timeout_c, ret);
+ }
+
+ s_media_buffer[1] = s_media_buffer[8];
+ s_media_buffer_32[1] = ret;
+ break;
+ }
+ case AMMBCommand::GetParambyDHCPExec:
+ {
+ const u32 value = s_media_buffer_32[2];
+
+ NOTICE_LOG_FMT(AMMEDIABOARD_NET, "GC-AM: GetParambyDHCPExec({})", value);
+
+ s_media_buffer[1] = 0;
+ s_media_buffer_32[1] = 0;
+ break;
+ }
+ case AMMBCommand::ModifyMyIPaddr:
+ AMMBCommandModifyMyIPaddr(2, NetworkCommandAddress2);
+ break;
+ case AMMBCommand::GetLastError:
+ {
+ const auto guest_socket = GuestSocket(s_media_buffer_32[2]);
+ const auto host_socket = GetHostSocket(guest_socket);
+
+ DEBUG_LOG_FMT(AMMEDIABOARD_NET, "GC-AM: GetLastError( {}({}) ):{}", host_socket,
+ int(guest_socket), int(s_last_error));
+
+ // Good enough, assuming it's called for the same socket right after an error.
+ // TODO: Implement something similar per socket.
+ s_media_buffer[1] = s_media_buffer[8];
+ s_media_buffer_32[1] = s_last_error;
+ }
+ break;
+ case AMMBCommand::InitLink:
+ NOTICE_LOG_FMT(AMMEDIABOARD_NET, "GC-AM: InitLink");
+ break;
+ case AMMBCommand::AllNetInit:
+ NOTICE_LOG_FMT(AMMEDIABOARD_NET, "GC-AM: AllNetInit");
+ break;
+ default:
+ ERROR_LOG_FMT(AMMEDIABOARD, "GC-AM: Command:{0:04x}", static_cast<u16>(ammb_command));
+ ERROR_LOG_FMT(AMMEDIABOARD, "GC-AM: Command Unhandled!");
+ break;
+ }
+
+ s_media_buffer[3] |= 0x80; // Command complete flag
+
+ memory.Memset(address, 0, length);
+
+ ExpansionInterface::GenerateInterrupt(0x10);
+ return 0;
+ }
+
+ // Max GC disc offset
+ if (offset >= 0x57058000)
+ {
+ PanicAlertFmtT("Unhandled Media Board Read: offset={0:08x} length={0:08x}", offset, length);
+ return 0;
+ }
+
+ if (s_firmware_map)
+ {
+ if (!SafeCopyToEmu(memory, address, s_firmware.data(), s_firmware.size(), offset, length))
+ {
+ ERROR_LOG_FMT(AMMEDIABOARD, "GC-AM: Invalid firmware buffer range: offset={}, length={}",
+ offset, length);
+ }
+ return 0;
+ }
+
+ if (const auto span = memory.GetSpanForAddress(address); span.size() < length)
+ {
+ ERROR_LOG_FMT(AMMEDIABOARD, "GC-AM: Invalid DIMM Disc read from: offset={}, length={}",
+ offset, length);
+ }
+ else if (s_dimm_disc->Read(offset, length, span.data()))
+ {
+ return 0;
+ }
+
+ return 1;
+ break;
+ case AMMBDICommand::Write:
+
+ // These two magic writes allow a new firmware to be programmed
+ if (offset == FirmwareMagicWrite1 && length == 0x20)
+ {
+ s_firmware_map = true;
+ return 0;
+ }
+
+ if (offset == FirmwareMagicWrite2 && length == 0x20)
+ {
+ s_firmware_map = true;
+ return 0;
+ }
+
+ if (s_firmware_map)
+ {
+ // Firmware memory (2MB)
+ if (offset >= 0x00400000 && offset <= 0x600000)
+ {
+ const u32 fw_offset = offset - 0x00400000;
+ if (!SafeCopyFromEmu(memory, s_firmware.data(), address, s_firmware.size(), fw_offset,
+ length))
+ {
+ ERROR_LOG_FMT(AMMEDIABOARD, "GC-AM: Invalid firmware write: offset={}, length={}",
+ fw_offset, length);
+ }
+ return 0;
+ }
+ }
+
+ // Network configuration
+ if (offset == 0x00000000 && length == 0x80)
+ {
+ FileWriteData(memory, &s_netcfg, 0, address, length);
+ return 0;
+ }
+
+ // media crc check on/off
+ if (offset == DIMMExtraSettings && length == 0x20)
+ {
+ FileWriteData(memory, &s_extra, 0, address, length);
+ return 0;
+ }
+
+ // Backup memory (8MB)
+ if (offset >= BackupMemory && offset < 0x00800000)
+ {
+ FileWriteData(memory, &s_backup, 0, address, length);
+ return 0;
+ }
+
+ // DIMM memory (8MB)
+ if (offset >= DIMMMemory && offset < 0x1F800000)
+ {
+ FileWriteData(memory, &s_dimm, offset - DIMMMemory, address, length);
+ return 0;
+ }
+
+ // Firmware Write
+ if (offset >= FirmwareAddress && offset < 0x84818000)
+ {
+ INFO_LOG_FMT(AMMEDIABOARD, "GC-AM: Write Firmware ({:08x})", offset);
+ PrintMBBuffer(address, length);
+ return 0;
+ }
+
+ // DIMM memory (8MB)
+ if (offset >= DIMMMemory2 && offset < 0xFF800000)
+ {
+ FileWriteData(memory, &s_dimm, offset - DIMMMemory2, address, length);
+ return 0;
+ }
+
+ if (offset == NetworkControl && length == 0x20)
+ {
+ FileWriteData(memory, &s_netctrl, 0, address, length);
+ return 0;
+ }
+
+ if (offset == DIMMCommandExecute1 && length == 0x20)
+ {
+ if (memory.Read_U8(address) == 1)
+ {
+ const AMMBCommand ammb_command = Common::BitCastPtr<AMMBCommand>(s_media_buffer + 0x22);
+
+ DEBUG_LOG_FMT(AMMEDIABOARD, "GC-AM: Execute command: (1):{0:04X}",
+ static_cast<u16>(ammb_command));
+
+ memset(s_media_buffer, 0, 0x20);
+
+ // Counter/Command
+ s_media_buffer_32[0] = s_media_buffer_32[8] | 0x80000000; // Set command okay flag
+
+ // Handle command
+ switch (ammb_command)
+ {
+ case AMMBCommand::Unknown_000:
+ s_media_buffer_32[1] = 1;
+ break;
+ case AMMBCommand::GetDIMMSize:
+ s_media_buffer_32[1] = 0x20000000;
+ break;
+ case AMMBCommand::GetMediaBoardStatus:
+ s_media_buffer_32[1] = LoadedGameProgram;
+ s_media_buffer_32[2] = 100;
+ break;
+ // SegaBoot version: 3.09
+ case AMMBCommand::GetSegaBootVersion:
+ // Version
+ s_media_buffer[4] = 0x03;
+ s_media_buffer[5] = 0x09;
+ // Unknown
+ s_media_buffer[6] = 1;
+ s_media_buffer_32[2] = 1;
+ s_media_buffer_32[4] = 0xFF;
+ break;
+ case AMMBCommand::GetSystemFlags:
+ s_media_buffer[4] = 1;
+ s_media_buffer[5] = GDROM;
+ // Enable development mode (Sega Boot)
+ // This also allows region free booting
+ s_media_buffer[6] = 1;
+ s_media_buffer[8] = 0; // Access Count
+ break;
+ case AMMBCommand::GetMediaBoardSerial:
+ memcpy(s_media_buffer + 4, "A89E-27A50364511", 16);
+ break;
+ case AMMBCommand::Unknown_104:
+ s_media_buffer[4] = 1;
+ break;
+ default:
+ PanicAlertFmtT("Unhandled Media Board Command:{0:04x}", static_cast<u16>(ammb_command));
+ break;
+ }
+
+ memset(s_media_buffer + 0x20, 0, 0x20);
+
+ ExpansionInterface::GenerateInterrupt(0x04);
+ return 0;
+ }
+ }
+
+ for (const auto& range : s_mediaboard_ranges)
+ {
+ if (offset >= range.start && offset < range.end)
+ {
+ DEBUG_LOG_FMT(AMMEDIABOARD, "GC-AM: Write MediaBoard ({:08x},{:08x},{:08x})", offset,
+ range.start, length);
+ SafeCopyFromEmu(memory, range.buffer, address, range.buffer_size, offset - range.start,
+ length);
+ PrintMBBuffer(address, length);
+ return 0;
+ }
+ }
+
+ // Max GC disc offset
+ if (offset >= 0x57058000)
+ {
+ PrintMBBuffer(address, length);
+ PanicAlertFmtT("Unhandled Media Board Write: offset={0:08x} length={0:08x}", offset, length);
+ }
+ break;
+ case AMMBDICommand::Execute:
+ {
+ const auto ammb_command = static_cast<AMMBCommand>(s_media_buffer_32[8] >> 16);
+ if (ammb_command == AMMBCommand::Unknown_000)
+ break;
+
+ if (offset == 0 && length == 0)
+ {
+ memset(s_media_buffer, 0, 0x20);
+
+ // Counter/Command
+ s_media_buffer_32[0] = s_media_buffer_32[8] | 0x80000000; // Set command okay flag
+
+ switch (ammb_command)
+ {
+ case AMMBCommand::Unknown_000:
+ s_media_buffer_32[1] = 1;
+ break;
+ case AMMBCommand::GetDIMMSize:
+ s_media_buffer_32[1] = 0x20000000;
+ break;
+ case AMMBCommand::GetMediaBoardStatus:
+ {
+ s_media_buffer_32[1] = s_board_status;
+ s_media_buffer_32[2] = s_load_progress;
+ if (s_load_progress < 100)
+ {
+ s_load_progress++;
+ }
+ else
+ {
+ s_board_status = LoadedGameProgram;
+ }
+ }
+ break;
+ // SegaBoot version: 3.11
+ case AMMBCommand::GetSegaBootVersion:
+ // Version
+ s_media_buffer[4] = 0x03;
+ s_media_buffer[5] = 0x11;
+ // Unknown
+ s_media_buffer[6] = 1;
+ s_media_buffer_32[2] = 1;
+ s_media_buffer_32[4] = 0xFF;
+ break;
+ case AMMBCommand::GetSystemFlags:
+ s_media_buffer[4] = 1;
+ s_media_buffer[5] = GDROM;
+ // Enable development mode (Sega Boot)
+ // This also allows region free booting
+ s_media_buffer[6] = 1;
+ s_media_buffer[8] = 0; // Access Count
+ break;
+ case AMMBCommand::GetMediaBoardSerial:
+ memcpy(s_media_buffer + 4, "A89E-27A50364511", 16);
+ break;
+ case AMMBCommand::Unknown_104:
+ s_media_buffer[4] = 1;
+ break;
+ case AMMBCommand::NetworkReInit:
+ break;
+ case AMMBCommand::TestHardware:
+ // Test type
+
+ // 0x01: Media board
+ // 0x04: Network
+
+ DEBUG_LOG_FMT(AMMEDIABOARD, "GC-AM: TestHardware: ({:08x})", s_media_buffer_32[11]);
+ // Pointer to a memory address that is directly displayed on screen as a string
+ DEBUG_LOG_FMT(AMMEDIABOARD, "GC-AM: ({:08x})", s_media_buffer_32[12]);
+
+ // On real systems it shows the status about the DIMM/GD-ROM here
+ // We just show "TEST OK"
+ memory.Write_U32(0x54534554, s_media_buffer_32[12]);
+ memory.Write_U32(0x004B4F20, s_media_buffer_32[12] + 4);
+
+ s_media_buffer_32[1] = s_media_buffer_32[9];
+ break;
+ case AMMBCommand::Closesocket:
+ AMMBCommandClosesocket(10);
+ break;
+ case AMMBCommand::Connect:
+ AMMBCommandConnect(10, NetworkCommandAddress1);
+ break;
+ case AMMBCommand::Recv:
+ AMMBCommandRecv(10, NetworkBufferAddress5);
+ break;
+ case AMMBCommand::Send:
+ AMMBCommandSend(10, NetworkBufferAddress1);
+ break;
+ case AMMBCommand::Socket:
+ AMMBCommandSocket(10);
+ break;
+ case AMMBCommand::Select:
+ AMMBCommandSelect(10, NetworkCommandAddress1);
+ break;
+ case AMMBCommand::SetSockOpt:
+ AMMBCommandSetSockOpt(10, NetworkCommandAddress1);
+ break;
+ case AMMBCommand::ModifyMyIPaddr:
+ AMMBCommandModifyMyIPaddr(10, NetworkCommandAddress1);
+ break;
+ // Empty reply
+ case AMMBCommand::InitLink:
+ NOTICE_LOG_FMT(AMMEDIABOARD_NET, "GC-AM: InitLink");
+ break;
+ case AMMBCommand::Unknown_605:
+ NOTICE_LOG_FMT(AMMEDIABOARD, "GC-AM: 0x605");
+ break;
+ case AMMBCommand::SetupLink:
+ {
+ const auto addra = std::bit_cast<Common::IPAddress>(s_media_buffer_32[12]);
+ const auto addrb = std::bit_cast<Common::IPAddress>(s_media_buffer_32[13]);
+
+ const u16 size = s_media_buffer[0x24] | s_media_buffer[0x25] << 8;
+ const u16 port = Common::swap16(s_media_buffer[0x27] | s_media_buffer[0x26] << 8);
+ const u16 unknown = s_media_buffer[0x2D] | s_media_buffer[0x2C] << 8;
+
+ NOTICE_LOG_FMT(AMMEDIABOARD_NET, "GC-AM: SetupLink:");
+ NOTICE_LOG_FMT(AMMEDIABOARD_NET, "GC-AM: Size: ({}) ", size);
+ NOTICE_LOG_FMT(AMMEDIABOARD_NET, "GC-AM: Port: ({})", port);
+ NOTICE_LOG_FMT(AMMEDIABOARD_NET, "GC-AM:LinkNum:({:02x})", s_media_buffer[0x28]);
+ NOTICE_LOG_FMT(AMMEDIABOARD_NET, "GC-AM: ({:02x})", s_media_buffer[0x2A]);
+ NOTICE_LOG_FMT(AMMEDIABOARD_NET, "GC-AM: ({:04x})", unknown);
+ NOTICE_LOG_FMT(AMMEDIABOARD_NET, "GC-AM: IP: ({})",
+ Common::IPAddressToString(addra)); // IP ?
+ NOTICE_LOG_FMT(AMMEDIABOARD_NET, "GC-AM: IP: ({})",
+ Common::IPAddressToString(addrb)); // Target IP
+ NOTICE_LOG_FMT(AMMEDIABOARD_NET, "GC-AM: ({:08x})",
+ Common::swap32(s_media_buffer_32[14])); // some RAM address
+ NOTICE_LOG_FMT(AMMEDIABOARD_NET, "GC-AM: ({:08x})",
+ Common::swap32(s_media_buffer_32[15])); // some RAM address
+
+ s_media_buffer_32[1] = 0;
+ }
+ break;
+ // This sends a UDP packet to previously defined Target IP/Port
+ case AMMBCommand::SearchDevices:
+ {
+ const u16 unknown = s_media_buffer[0x25] | s_media_buffer[0x24] << 8;
+ const u16 off = s_media_buffer[0x26] | s_media_buffer[0x27] << 8;
+ const u32 addr = s_media_buffer_32[10];
+
+ NOTICE_LOG_FMT(AMMEDIABOARD_NET, "GC-AM: SearchDevices: ({})", unknown);
+ NOTICE_LOG_FMT(AMMEDIABOARD_NET, "GC-AM: Offset: ({:04x})", off);
+ NOTICE_LOG_FMT(AMMEDIABOARD_NET, "GC-AM: ({:08x})", addr);
+
+ if (!NetworkBufferCheck(off + addr - NetworkBufferAddress2, 0x20))
+ {
+ break;
+ }
+
+ const u8* const data = s_network_buffer.data() + (off + addr - NetworkBufferAddress2);
+
+ for (u32 i = 0; i < 0x20; i += 0x10)
+ {
+ const std::array<u32, 4> data_u32 = Common::BitCastPtr<std::array<u32, 4>>(data + i);
+ NOTICE_LOG_FMT(AMMEDIABOARD_NET, "GC-AM: {:08x} {:08x} {:08x} {:08x}", data_u32[0],
+ data_u32[1], data_u32[2], data_u32[3]);
+ }
+
+ s_media_buffer_32[1] = 0;
+ }
+ break;
+ case AMMBCommand::Unknown_608:
+ {
+ const u32 ip = s_media_buffer_32[10];
+ const u16 port = Common::swap16(s_media_buffer[6] | s_media_buffer[7] << 8);
+ const u16 flag = s_media_buffer[10] | s_media_buffer[11] << 8;
+
+ NOTICE_LOG_FMT(AMMEDIABOARD_NET, "GC-AM: 0x608( {} {} {} )", ip, port, flag);
+ }
+ break;
+ case AMMBCommand::Unknown_614:
+ NOTICE_LOG_FMT(AMMEDIABOARD_NET, "GC-AM: 0x614");
+ break;
+ default:
+ ERROR_LOG_FMT(AMMEDIABOARD, "GC-AM: Execute buffer UNKNOWN:{0:04x}",
+ static_cast<u16>(ammb_command));
+ break;
+ }
+
+ memset(s_media_buffer + 0x20, 0, 0x20);
+ return 0;
+ }
+
+ PanicAlertFmtT("Unhandled Media Board Execute:{0:04x}", static_cast<u16>(ammb_command));
+ break;
+ }
+ default:
+ PanicAlertFmtT("Unhandled Media Board Command:{0:02x}", command);
+ break;
+ }
+
+ return 0;
+}
+
+u32 GetMediaType()
+{
+ switch (GetGameType())
+ {
+ default:
+ case FZeroAX:
+ case VirtuaStriker3:
+ case VirtuaStriker4:
+ case VirtuaStriker4_2006:
+ case GekitouProYakyuu:
+ case KeyOfAvalon:
+ return GDROM;
+
+ case MarioKartGP:
+ case MarioKartGP2:
+ case FZeroAXMonster:
+ return NAND;
+ }
+}
+
+// This is checking for the real game IDs (See boot.id within the game)
+u32 GetGameType()
+{
+ u16 triforce_id = 0;
+ const std::string& game_id = SConfig::GetInstance().GetGameID();
+
+ if (game_id.length() == 6)
+ {
+ triforce_id = game_id[1] << 8 | game_id[2];
+ }
+ else
+ {
+ triforce_id = 0x454A; // Fallback (VirtuaStriker3)
+ }
+
+ auto it = s_game_map.find(triforce_id);
+ if (it != s_game_map.end())
+ {
+ return it->second;
+ }
+
+ PanicAlertFmtT("Unknown game ID:{0:08x}, using default controls.", triforce_id);
+ return VirtuaStriker3; // Fallback
+}
+
+bool GetTestMenu()
+{
+ return s_test_menu;
+}
+
+static void CloseAllSockets()
+{
+ for (u32 i = FIRST_VALID_FD; i < std::size(s_sockets); ++i)
+ {
+ if (s_sockets[i] != SOCKET_ERROR)
+ {
+ closesocket(s_sockets[i]);
+ s_sockets[i] = SOCKET_ERROR;
+ }
+ }
+}
+
+void Shutdown()
+{
+ s_netcfg.Close();
+ s_netctrl.Close();
+ s_extra.Close();
+ s_backup.Close();
+ s_dimm.Close();
+
+ s_dimm_disc.reset();
+
+ CloseAllSockets();
+}
+
+void DoState(PointerWrap& p)
+{
+ p.Do(s_firmware_map);
+ p.Do(s_test_menu);
+ p.Do(s_timeouts);
+ p.Do(s_last_error);
+ p.Do(s_gcam_key_a);
+ p.Do(s_gcam_key_b);
+ p.Do(s_gcam_key_c);
+ p.Do(s_firmware);
+ p.Do(s_media_buffer_32);
+ p.Do(s_network_command_buffer);
+ p.Do(s_network_buffer);
+ p.Do(s_allnet_buffer);
+ p.Do(s_allnet_settings);
+
+ p.Do(s_game_modified_ip_address);
+
+ p.Do(s_board_status);
+ p.Do(s_load_progress);
+
+ // TODO: Handle the files better.
+ // Data corruption is probably currently possible.
+
+ // s_netcfg
+ // s_netctrl
+ // s_extra
+ // s_backup
+ // s_dimm
+
+ // TODO: Handle sockets better.
+ // For now, we just recreate a TCP socket for any socket that existed.
+ // We should probably re-bind sockets and handle UDP sockets.
+
+ GuestFdSet created_sockets{};
+ if (p.IsWriteMode() || p.IsVerifyMode())
+ {
+ for (u32 i = FIRST_VALID_FD; i < std::size(s_sockets); ++i)
+ {
+ if (s_sockets[i] != SOCKET_ERROR)
+ created_sockets.SetFd(GuestSocket(i));
+ }
+ }
+
+ p.Do(created_sockets);
+
+ if (p.IsReadMode())
+ {
+ CloseAllSockets();
+
+ for (u32 i = FIRST_VALID_FD; i < std::size(s_sockets); ++i)
+ {
+ if (!created_sockets.IsFdSet(GuestSocket(i)))
+ continue;
+
+ s_sockets[i] = socket(AF_INET, SOCK_STREAM, 0);
+ }
+ }
+}
+
+s32 DebuggerGetSocket(u32 triforce_fd)
+{
+ if (triforce_fd < std::size(s_sockets))
+ return s32(s_sockets[triforce_fd]);
+
+ WARN_LOG_FMT(AMMEDIABOARD, "GC-AM: Bad socket fd used by the debugger: {}", triforce_fd);
+ return -1;
+}
+} // namespace AMMediaboard
diff --git a/Source/Core/Core/HW/DVD/AMMediaboard.h b/Source/Core/Core/HW/DVD/AMMediaboard.h
new file mode 100644
index 0000000000..4fa649c8bb
--- /dev/null
+++ b/Source/Core/Core/HW/DVD/AMMediaboard.h
@@ -0,0 +1,274 @@
+// Copyright 2025 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <array>
+#include <optional>
+#include <span>
+#include <string_view>
+
+#include "Common/ChunkFile.h"
+#include "Common/CommonTypes.h"
+#include "Common/Network.h"
+
+#include "DiscIO/Volume.h"
+
+namespace Core
+{
+class CPUThreadGuard;
+class System;
+} // namespace Core
+
+namespace AMMediaboard
+{
+
+enum GameType
+{
+ FZeroAX = 1,
+ FZeroAXMonster,
+ MarioKartGP,
+ MarioKartGP2,
+ VirtuaStriker3,
+ VirtuaStriker4,
+ VirtuaStriker4_2006,
+ GekitouProYakyuu,
+ KeyOfAvalon,
+ FirmwareUpdate,
+};
+
+enum MediaType
+{
+ GDROM = 1,
+ NAND,
+};
+
+enum MediaBoardType
+{
+ NANDMaskBoardHDD = 0,
+ NANDMaskBoardMask = 1,
+ NANDMaskBoardNAND = 2,
+ DIMMBoardType3 = 4,
+};
+
+enum MediaBoardStatus
+{
+ Initializing = 0,
+ CheckingNetwork = 1,
+ SystemDisc = 2,
+ TestingGameProgram = 3,
+ LoadingGameProgram = 4,
+ LoadedGameProgram = 5,
+ Error = 6,
+};
+
+enum InquiryType
+{
+ Version1 = 0x21484100,
+ Version2 = 0x29484100,
+};
+
+struct MediaBoardRange
+{
+ u32 start;
+ u32 end;
+ u8* buffer;
+ std::size_t buffer_size;
+
+ MediaBoardRange(u32 start_, u32 size_, std::span<u8> buffer_);
+};
+
+enum class AMMBDICommand : u16
+{
+ Inquiry = 0x12,
+ Read = 0xA8,
+ Write = 0xAA,
+ Execute = 0xAB,
+};
+
+enum class AMMBCommand : u16
+{
+ Unknown_000 = 0x000,
+ GetDIMMSize = 0x001,
+
+ GetMediaBoardStatus = 0x100,
+ GetSegaBootVersion = 0x101,
+ GetSystemFlags = 0x102,
+ GetMediaBoardSerial = 0x103,
+ Unknown_104 = 0x104,
+
+ NetworkReInit = 0x204,
+
+ TestHardware = 0x301,
+
+ // Network used by Mario Kart GPs
+ Accept = 0x401,
+ Bind = 0x402,
+ Closesocket = 0x403,
+ Connect = 0x404,
+ GetIPbyDNS = 0x405,
+ InetAddr = 0x406,
+ Ioctl = 0x407, // Unused
+ Listen = 0x408,
+ Recv = 0x409,
+ Send = 0x40A,
+ Socket = 0x40B,
+ Select = 0x40C,
+ Shutdown = 0x40D, // Unused
+ SetSockOpt = 0x40E,
+ GetSockOpt = 0x40F, // Unused
+ SetTimeOuts = 0x410,
+ GetLastError = 0x411,
+ RouteAdd = 0x412, // Unused
+ RouteDelete = 0x413, // Unused
+ GetParambyDHCPExec = 0x414,
+ ModifyMyIPaddr = 0x415,
+ Recvfrom = 0x416, // Unused
+ Sendto = 0x417, // Unused
+ RecvDimmImage = 0x418, // Unused
+ SendDimmImage = 0x419, // Unused
+
+ // Network used by F-Zero AX
+ InitLink = 0x601,
+ Unknown_605 = 0x605,
+ SetupLink = 0x606,
+ SearchDevices = 0x607,
+ Unknown_608 = 0x608,
+ Unknown_614 = 0x614,
+
+ // All.Net used by Virtua Striker 4 2006 (Japan)
+ AllNetInit = 0x700,
+ Unknown_701 = 0x701,
+ Unknown_702 = 0x702,
+ Unknown_703 = 0x703,
+ Unknown_704 = 0x704,
+
+ // NETDIMM Commands
+ Unknown_001 = 0x001,
+ GetNetworkFirmVersion = 0x101,
+ Unknown_103 = 0x103,
+};
+
+enum MediaBoardAddress : u32
+{
+ MediaBoardStatus1 = 0x80000000,
+ MediaBoardStatus2 = 0x80000020,
+ MediaBoardStatus3 = 0x80000040,
+
+ FirmwareStatus1 = 0x80000120,
+ FirmwareStatus2 = 0x80000140,
+
+ BackupMemory = 0x000006A0,
+
+ DIMMMemory = 0x1F000000,
+ DIMMMemory2 = 0xFF000000,
+
+ DIMMExtraSettings = 0x1FFEFFE0,
+
+ AllNetSettings = 0x1EFF8000,
+
+ NetworkControl = 0xFFFF0000,
+
+ DIMMCommandVersion1 = 0x1F900000,
+ DIMMCommandVersion2 = 0x84000000,
+ DIMMCommandVersion2_2 = 0x89000000,
+ DIMMCommandExecute1 = 0x84000040,
+ DIMMCommandExecute2 = 0x88000000,
+
+ NetworkCommandAddress1 = 0x1F800200,
+ NetworkCommandAddress2 = 0x89040200,
+
+ NetworkBufferAddress1 = 0x1FA00000,
+ NetworkBufferAddress2 = 0x1FD00000,
+ NetworkBufferAddress3 = 0x89100000,
+ NetworkBufferAddress4 = 0x89180000,
+ NetworkBufferAddress5 = 0x1FB00000,
+
+ AllNetBuffer = 0x89010000,
+
+ FirmwareAddress = 0x84800000,
+
+ FirmwareMagicWrite1 = 0x00600000,
+ FirmwareMagicWrite2 = 0x00700000,
+};
+
+// Mario Kart GP2 has a complete list of them
+// but in japanese
+// They somewhat match WSA errors codes
+enum SocketStatusCodes
+{
+ SSC_E_4 = -4, // Failure (abnormal argument)
+ SSC_E_3 = -3, // Success (unsupported command)
+ SSC_E_2 =
+ -2, // Failure (failed to send, abnormal argument, or communication condition violation)
+ SSC_E_1 = -1, // Failure (error termination)
+
+ SSC_EINTR = 4, // An interrupt occurred before data reception was completed
+ SSC_EBADF = 9, // Invalid descriptor
+ SSC_E_11 = 11, // Send operation was blocked on a non-blocking mode socket
+ SSC_EACCES = 13, // The socket does not support broadcast addresses, but the destination address
+ // is a broadcast address
+ SSC_EFAULT =
+ 14, // The name argument specifies a location other than an address used by the process.
+ SSC_E_23 = 23, // System file table is full.
+ SSC_AEMFILE = 24, // Process descriptor table is full.
+ SSC_EMSGSIZE =
+ 36, // Socket tried to send message without splitting, but message size is too large.
+ SSC_EAFNOSUPPORT = 47, // Address prohibited for use on this socket.
+ SSC_EADDRINUSE = 48, // Address already in use.
+ SSC_EADDRNOTAVAIL = 49, // Prohibited address.
+ SSC_E_50 = 50, // Non-socket descriptor.
+ SSC_ENETUNREACH = 51, // Cannot access specified network.
+ SSC_ENOBUFS = 55, // Insufficient buffer
+ SSC_EISCONN = 56, // Already connected socket
+ SSC_ENOTCONN = 57, // No connection for connection-type socket
+ SSC_ETIMEDOUT = 60, // Timeout
+ SSC_ECONNREFUSED = 61, // Connection request forcibly rejected
+ SSC_EHOSTUNREACH = 65, // Remote host cannot be reached
+ SSC_EHOSTDOWN = 67, // Remote host is down
+ SSC_EWOULDBLOCK = 70, // Socket is in non-blocking mode and connection has not been completed
+ SSC_E_69 = 69, // Socket is in non-blocking mode and a previously issued Connect command has not
+ // been completed
+ SSC_SUCCESS = 70,
+};
+
+static constexpr std::size_t SOCKET_FD_MAX = 64;
+
+void Init();
+void FirmwareMap(bool on);
+void InitDIMM(const DiscIO::Volume& volume);
+void InitKeys(u32 key_a, u32 key_b, u32 key_c);
+u32 ExecuteCommand(std::array<u32, 3>& dicmd_buf, u32* diimm_buf, u32 address, u32 length);
+u32 GetGameType();
+u32 GetMediaType();
+bool GetTestMenu();
+void Shutdown();
+void DoState(PointerWrap& p);
+
+struct ParsedIPRedirection
+{
+ std::string_view original;
+ std::string_view replacement;
+ std::string_view description;
+};
+
+std::optional<ParsedIPRedirection> ParseIPRedirection(std::string_view str);
+
+struct IPRedirection
+{
+ Common::IPv4PortRange original;
+ Common::IPv4PortRange replacement;
+
+ // Caller should check if it matches first!
+ Common::IPv4Port Apply(Common::IPv4Port subject) const;
+ Common::IPv4Port Reverse(Common::IPv4Port subject) const;
+ std::string ToString() const;
+};
+
+using IPRedirections = std::vector<IPRedirection>;
+
+IPRedirections GetIPRedirections();
+
+s32 DebuggerGetSocket(u32 triforce_fd);
+
+}; // namespace AMMediaboard
diff --git a/Source/Core/Core/HW/DVD/DVDInterface.cpp b/Source/Core/Core/HW/DVD/DVDInterface.cpp
index efb279cfac..33b0efc548 100644
--- a/Source/Core/Core/HW/DVD/DVDInterface.cpp
+++ b/Source/Core/Core/HW/DVD/DVDInterface.cpp
@@ -26,6 +26,7 @@
#include "Core/CoreTiming.h"
#include "Core/DolphinAnalytics.h"
#include "Core/HW/AudioInterface.h"
+#include "Core/HW/DVD/AMMediaboard.h"
#include "Core/HW/DVD/DVDMath.h"
#include "Core/HW/DVD/DVDThread.h"
#include "Core/HW/EXI/EXI_DeviceIPL.h"
@@ -123,6 +124,12 @@ void DVDInterface::DoState(PointerWrap& p)
m_system.GetDVDThread().DoState(p);
m_adpcm_decoder.DoState(p);
+
+ if (m_system.IsTriforce())
+ {
+ AMMediaboard::DoState(p);
+ p.DoMarker("AMMediaboard");
+ }
}
size_t DVDInterface::ProcessDTKSamples(s16* target_samples, size_t target_block_count,
@@ -281,6 +288,17 @@ void DVDInterface::Init()
u64 userdata = PackFinishExecutingCommandUserdata(ReplyType::DTK, DIInterruptType::TCINT);
core_timing.ScheduleEvent(0, m_finish_executing_command, userdata);
+
+ if (m_system.IsTriforce())
+ {
+ AMMediaboard::Init();
+
+ // The Triforce IPL expects the cover to be closed
+ m_DICVR.Hex = 0;
+ // The Triforce IPL checks this bit to set the physical memory to
+ // either 50MB(unset) or 24MB(set)
+ m_DICFG.Hex |= 8;
+ }
}
// Resets state on the MN102 chip in the drive itself, but not the DI registers exposed on the
@@ -310,7 +328,7 @@ void DVDInterface::ResetDrive(bool spinup)
else if (!spinup)
{
// Wii hardware tests indicate that this is used when ejecting and inserting a new disc, or
- // performing a reset without spinup.
+ // performing a reset without spin-up.
SetDriveState(DriveState::DiscChangeDetected);
}
else
@@ -330,6 +348,11 @@ void DVDInterface::ResetDrive(bool spinup)
void DVDInterface::Shutdown()
{
m_system.GetDVDThread().Stop();
+
+ if (m_system.IsTriforce())
+ {
+ AMMediaboard::Shutdown();
+ }
}
static u64 GetDiscEndOffset(const DiscIO::VolumeDisc& disc)
@@ -697,11 +720,16 @@ bool DVDInterface::CheckReadPreconditions()
SetDriveError(DriveError::MotorStopped);
return false;
}
- if (m_drive_state == DriveState::DiscIdNotRead)
+
+ // SegaBoot doesn't read the Disc ID
+ if (!m_system.IsTriforce())
{
- ERROR_LOG_FMT(DVDINTERFACE, "Disc id not read.");
- SetDriveError(DriveError::NoDiscID);
- return false;
+ if (m_drive_state == DriveState::DiscIdNotRead)
+ {
+ ERROR_LOG_FMT(DVDINTERFACE, "Disc id not read.");
+ SetDriveError(DriveError::NoDiscID);
+ return false;
+ }
}
return true;
}
@@ -753,12 +781,19 @@ void DVDInterface::ExecuteCommand(ReplyType reply_type)
DIInterruptType interrupt_type = DIInterruptType::TCINT;
bool command_handled_by_thread = false;
- // Swaps endian of Triforce DI commands, and zeroes out random bytes to prevent unknown read
- // subcommand errors
- auto& dvd_thread = m_system.GetDVDThread();
- if (dvd_thread.HasDisc() && dvd_thread.GetDiscType() == DiscIO::Platform::Triforce)
+ if (m_system.IsTriforce())
{
- // TODO(C++23): Use std::byteswap and a bitwise AND for increased clarity
+ if (!AMMediaboard::ExecuteCommand(m_DICMDBUF, &m_DIIMMBUF, m_DIMAR, m_DILENGTH))
+ {
+ // Transfer is done
+ m_DICR.TSTART = 0;
+ m_DIMAR += m_DILENGTH;
+ m_DILENGTH = 0;
+ GenerateDIInterrupt(DIInterruptType::TCINT);
+ m_error_code = DriveError::None;
+ return;
+ }
+ // Normal read command pass on to normal handling
m_DICMDBUF[0] <<= 24;
}
diff --git a/Source/Core/Core/HW/EXI/BBA/TAPServerConnection.cpp b/Source/Core/Core/HW/EXI/BBA/TAPServerConnection.cpp
index f2bd89e6b2..570221c72b 100644
--- a/Source/Core/Core/HW/EXI/BBA/TAPServerConnection.cpp
+++ b/Source/Core/Core/HW/EXI/BBA/TAPServerConnection.cpp
@@ -31,11 +31,7 @@ using ws_ssize_t = int;
using ws_ssize_t = ssize_t;
#endif
-#ifdef __linux__
-#define SEND_FLAGS MSG_NOSIGNAL
-#else
-#define SEND_FLAGS 0
-#endif
+using Common::SEND_FLAGS;
TAPServerConnection::TAPServerConnection(const std::string& destination,
std::function<void(std::string&&)> recv_cb,
@@ -114,11 +110,7 @@ static int ConnectToDestination(const std::string& destination)
return -1;
}
-#ifdef __APPLE__
- int opt_no_sigpipe = 1;
- if (setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &opt_no_sigpipe, sizeof(opt_no_sigpipe)) < 0)
- INFO_LOG_FMT(SP1, "Failed to set SO_NOSIGPIPE on socket\n");
-#endif
+ Common::SetPlatformSocketOptions(fd);
if (connect(fd, reinterpret_cast<sockaddr*>(&ss), ss_size) == -1)
{
diff --git a/Source/Core/Core/HW/EXI/EXI.cpp b/Source/Core/Core/HW/EXI/EXI.cpp
index d676dc4845..144fdd57b9 100644
--- a/Source/Core/Core/HW/EXI/EXI.cpp
+++ b/Source/Core/Core/HW/EXI/EXI.cpp
@@ -66,18 +66,6 @@ void ExpansionInterfaceManager::AddMemoryCard(Slot slot)
m_channels[SlotToEXIChannel(slot)]->AddDevice(memorycard_device, SlotToEXIDevice(slot));
}
-void ExpansionInterfaceManager::AddSP1Device()
-{
- EXIDeviceType sp1_device = EXIDeviceType::Baseboard;
- auto& system = Core::System::GetInstance();
- if (system.IsTriforce())
- {
- sp1_device = Config::Get(Config::MAIN_SERIAL_PORT_1);
- }
-
- m_channels[0]->AddDevice(sp1_device, SlotToEXIDevice(Slot::SP1));
-}
-
u8 SlotToEXIChannel(Slot slot)
{
switch (slot)
@@ -157,9 +145,15 @@ void ExpansionInterfaceManager::Init(const Sram* override_sram)
AddMemoryCard(slot);
m_channels[0]->AddDevice(EXIDeviceType::MaskROM, 1);
- AddSP1Device();
- m_channels[SlotToEXIChannel(Slot::SP1)]->AddDevice(Config::Get(Config::MAIN_SERIAL_PORT_1),
- SlotToEXIDevice(Slot::SP1));
+
+ // Automatically connect the Triforce Baseboard in Triforce mode
+ EXIDeviceType sp1_device = Config::Get(Config::MAIN_SERIAL_PORT_1);
+ if (m_system.IsTriforce())
+ {
+ sp1_device = EXIDeviceType::Baseboard;
+ }
+
+ m_channels[SlotToEXIChannel(Slot::SP1)]->AddDevice(sp1_device, SlotToEXIDevice(Slot::SP1));
m_channels[SlotToEXIChannel(Slot::SP2)]->AddDevice(Config::Get(Config::MAIN_SERIAL_PORT_2),
SlotToEXIDevice(Slot::SP2));
diff --git a/Source/Core/Core/HW/EXI/EXI.h b/Source/Core/Core/HW/EXI/EXI.h
index f0ea5fbfb5..7ded905b98 100644
--- a/Source/Core/Core/HW/EXI/EXI.h
+++ b/Source/Core/Core/HW/EXI/EXI.h
@@ -89,7 +89,6 @@ public:
private:
void AddMemoryCard(Slot slot);
- void AddSP1Device();
static void ChangeDeviceCallback(Core::System& system, u64 userdata, s64 cycles_late);
static void UpdateInterruptsCallback(Core::System& system, u64 userdata, s64 cycles_late);
diff --git a/Source/Core/Core/HW/EXI/EXI_Device.cpp b/Source/Core/Core/HW/EXI/EXI_Device.cpp
index e5ee2d70c5..ab92aa5be6 100644
--- a/Source/Core/Core/HW/EXI/EXI_Device.cpp
+++ b/Source/Core/Core/HW/EXI/EXI_Device.cpp
@@ -20,6 +20,8 @@
#ifdef HAVE_CUBEB
#include "Core/HW/EXI/EXI_DeviceMic.h"
+#else
+#include "Common/MsgHandler.h"
#endif
namespace ExpansionInterface
diff --git a/Source/Core/Core/HW/EXI/EXI_DeviceBaseboard.cpp b/Source/Core/Core/HW/EXI/EXI_DeviceBaseboard.cpp
index 7df13f5020..c285d69189 100644
--- a/Source/Core/Core/HW/EXI/EXI_DeviceBaseboard.cpp
+++ b/Source/Core/Core/HW/EXI/EXI_DeviceBaseboard.cpp
@@ -1,20 +1,107 @@
-// Copyright 2013 Dolphin Emulator Project
+// Copyright 2025 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "Core/HW/EXI/EXI_DeviceBaseboard.h"
-#include "Common/ChunkFile.h"
+#include <numeric>
+#include <string>
+
+#include <fmt/format.h>
+
+#include "Common/BitUtils.h"
+#include "Common/Buffer.h"
#include "Common/CommonTypes.h"
+#include "Common/FileUtil.h"
+#include "Common/IOFile.h"
#include "Common/Logging/Log.h"
+#include "Core/ConfigManager.h"
+#include "Core/Core.h"
+#include "Core/HW/DVD/AMMediaboard.h"
+#include "Core/HW/EXI/EXI.h"
+#include "Core/HW/Memmap.h"
+#include "Core/System.h"
+
+static bool s_interrupt_set = false;
+static u32 s_irq_timer = 0;
+static u32 s_irq_status = 0;
+
+static u16 CheckSum(const u8* data, u32 length)
+{
+ return std::accumulate(data, data + length, 0);
+}
+
namespace ExpansionInterface
{
+
+void GenerateInterrupt(int flag)
+{
+ auto& system = Core::System::GetInstance();
+
+ s_interrupt_set = true;
+ s_irq_timer = 0;
+ s_irq_status = flag;
+
+ system.GetExpansionInterface().UpdateInterrupts();
+}
+
CEXIBaseboard::CEXIBaseboard(Core::System& system) : IEXIDevice(system)
{
+ std::string backup_filename(fmt::format("{}tribackup_{}.bin", File::GetUserPath(D_TRIUSER_IDX),
+ SConfig::GetInstance().GetGameID()));
+
+ m_backup = File::IOFile(backup_filename, "rb+");
+ if (!m_backup.IsOpen())
+ {
+ m_backup = File::IOFile(backup_filename, "wb+");
+ }
+
+ // Some games share the same ID Client/Server
+ if (!m_backup.IsGood())
+ {
+ PanicAlertFmt("Failed to open {}\nFile might be in use.", backup_filename.c_str());
+
+ std::srand(static_cast<u32>(std::time(nullptr)));
+
+ backup_filename = fmt::format("{}tribackup_tmp_{}{}.bin", File::GetUserPath(D_TRIUSER_IDX),
+ rand(), SConfig::GetInstance().GetGameID());
+
+ m_backup = File::IOFile(backup_filename, "wb+");
+ }
+
+ // Virtua Striker 4 and Gekitou Pro Yakyuu need a higher FIRM version
+ // Which is read from the backup data?!
+ if (AMMediaboard::GetGameType() == AMMediaboard::VirtuaStriker4 ||
+ AMMediaboard::GetGameType() == AMMediaboard::GekitouProYakyuu)
+ {
+ const u64 backup_size = m_backup.GetSize();
+ if (backup_size >= 0x20C + 0x1F4)
+ {
+ Common::UniqueBuffer<u8> data(backup_size);
+ m_backup.ReadBytes(data.data(), data.size());
+
+ // Set FIRM version
+ Common::BitCastPtr<u16>(data.data() + 0x12) = 0x1703;
+ Common::BitCastPtr<u16>(data.data() + 0x212) = 0x1703;
+
+ // Update checksum
+ Common::BitCastPtr<u16>(data.data() + 0x0A) = Common::swap16(CheckSum(&data[0xC], 0x1F4));
+ Common::BitCastPtr<u16>(data.data() + 0x20A) = Common::swap16(CheckSum(&data[0x20C], 0x1F4));
+
+ m_backup.Seek(0, File::SeekOrigin::Begin);
+ m_backup.WriteBytes(data.data(), data.size());
+ m_backup.Flush();
+ }
+ }
+}
+
+CEXIBaseboard::~CEXIBaseboard()
+{
}
void CEXIBaseboard::SetCS(int cs)
{
+ DEBUG_LOG_FMT(SP1, "AM-BB: ChipSelect={}", cs);
if (cs)
m_position = 0;
}
@@ -24,33 +111,203 @@ bool CEXIBaseboard::IsPresent() const
return true;
}
+bool CEXIBaseboard::IsInterruptSet()
+{
+ if (s_interrupt_set)
+ {
+ DEBUG_LOG_FMT(SP1, "AM-BB: IRQ");
+ if (++s_irq_timer > 12)
+ s_interrupt_set = false;
+ return true;
+ }
+
+ return false;
+}
+
+void CEXIBaseboard::DMAWrite(u32 addr, u32 size)
+{
+ const auto& system = Core::System::GetInstance();
+ const auto& memory = system.GetMemory();
+ auto span = memory.GetSpanForAddress(addr);
+
+ if (span.size() < size)
+ {
+ ERROR_LOG_FMT(SP1, "AM-BB: Backup DMA Write overflow: address=0x{:08x}, length={}, span={}",
+ addr, size, span.size());
+ return;
+ }
+ NOTICE_LOG_FMT(SP1, "AM-BB: COMMAND: Backup DMA Write: {:08x} {:x}", addr, size);
+
+ m_backup.Seek(m_backup_offset, File::SeekOrigin::Begin);
+ m_backup.WriteBytes(span.data(), size);
+ m_backup.Flush();
+}
+
+void CEXIBaseboard::DMARead(u32 addr, u32 size)
+{
+ const auto& system = Core::System::GetInstance();
+ const auto& memory = system.GetMemory();
+ auto span = memory.GetSpanForAddress(addr);
+
+ if (span.size() < size)
+ {
+ ERROR_LOG_FMT(SP1, "AM-BB: Backup DMA Read overflow: address=0x{:08x}, length={}, span={}",
+ addr, size, span.size());
+ return;
+ }
+ NOTICE_LOG_FMT(SP1, "AM-BB: COMMAND: Backup DMA Read: {:08x} {:x}", addr, size);
+
+ m_backup.Seek(m_backup_offset, File::SeekOrigin::Begin);
+ m_backup.ReadBytes(span.data(), size);
+}
+
void CEXIBaseboard::TransferByte(u8& byte)
{
- if (m_position == 0)
+ DEBUG_LOG_FMT(SP1, "AM-BB: > {:02x}", byte);
+ if (m_position < 4)
{
- m_command = byte;
+ m_command[m_position] = byte;
+ byte = 0xFF;
}
- else
+
+ if ((m_position >= 2) && (m_command[0] == 0 && m_command[1] == 0))
{
- switch (m_command)
+ // Read serial ID
+ byte = "\x06\x04\x10\x00"[(m_position - 2) & 3];
+ }
+ else if (m_position == 3)
+ {
+ u32 checksum = (m_command[0] << 24) | (m_command[1] << 16) | (m_command[2] << 8);
+ u32 bit = 0x80000000UL;
+ u32 check = 0x8D800000UL;
+ while (bit >= 0x100)
{
- case 0x00:
+ if (checksum & bit)
+ checksum ^= check;
+ check >>= 1;
+ bit >>= 1;
+ }
+
+ if (m_command[3] != (checksum & 0xFF))
+ DEBUG_LOG_FMT(SP1, "AM-BB: cs: {:02x}, w: {:02x}", m_command[3], checksum & 0xFF);
+ }
+ else if (m_position == 4)
+ {
+ switch (m_command[0])
{
- static constexpr std::array<u8, 4> ID = {0x06, 0x04, 0x10, 0x00};
- byte = ID[(m_position - 2) & 3];
+ case BackupOffsetSet:
+ m_backup_offset = (m_command[1] << 8) | m_command[2];
+ DEBUG_LOG_FMT(SP1, "AM-BB: COMMAND: BackupOffsetSet:{:04x}", m_backup_offset);
+ m_backup.Seek(m_backup_offset, File::SeekOrigin::Begin);
+ byte = 0x01;
+ break;
+ case BackupWrite:
+ DEBUG_LOG_FMT(SP1, "AM-BB: COMMAND: BackupWrite:{:04x}-{:02x}", m_backup_offset,
+ m_command[1]);
+ m_backup.WriteBytes(&m_command[1], 1);
+ m_backup.Flush();
+ byte = 0x01;
+ break;
+ case BackupRead:
+ DEBUG_LOG_FMT(SP1, "AM-BB: COMMAND: BackupRead :{:04x}", m_backup_offset);
+ byte = 0x01;
+ break;
+ case DMAOffsetLengthSet:
+ m_backup_dma_offset = (m_command[1] << 8) | m_command[2];
+ m_backup_dma_length = m_command[3];
+ NOTICE_LOG_FMT(SP1, "AM-BB: COMMAND: DMAOffsetLengthSet :{:04x} {:02x}", m_backup_dma_offset,
+ m_backup_dma_length);
+ byte = 0x01;
+ break;
+ case ReadISR:
+ NOTICE_LOG_FMT(SP1, "AM-BB: COMMAND: ReadISR :{:02x} {:02x}:{:02x} {:02x}", m_command[1],
+ m_command[2], 4, s_irq_status);
+ byte = 0x04;
+ break;
+ case WriteISR:
+ NOTICE_LOG_FMT(SP1, "AM-BB: COMMAND: WriteISR :{:02x} {:02x}", m_command[1], m_command[2]);
+ s_irq_status &= ~(m_command[2]);
+ byte = 0x04;
+ break;
+ // 2 byte out
+ case ReadIMR:
+ NOTICE_LOG_FMT(SP1, "AM-BB: COMMAND: ReadIMR :{:02x} {:02x}", m_command[1], m_command[2]);
+ byte = 0x04;
+ break;
+ case WriteIMR:
+ NOTICE_LOG_FMT(SP1, "AM-BB: COMMAND: WriteIMR :{:02x} {:02x}", m_command[1], m_command[2]);
+ byte = 0x04;
+ break;
+ case WriteLANCNT:
+ NOTICE_LOG_FMT(SP1, "AM-BB: COMMAND: WriteLANCNT :{:02x} {:02x}", m_command[1], m_command[2]);
+ if ((m_command[1] == 0) && (m_command[2] == 0))
+ {
+ s_interrupt_set = true;
+ s_irq_timer = 0;
+ s_irq_status = 0x02;
+ }
+ if ((m_command[1] == 2) && (m_command[2] == 1))
+ {
+ s_irq_status = 0;
+ }
+ byte = 0x08;
+ break;
+ default:
+ byte = 4;
+ ERROR_LOG_FMT(SP1, "AM-BB: COMMAND: {:02x} {:02x} {:02x}", m_command[0], m_command[1],
+ m_command[2]);
break;
}
+ }
+ else if (m_position > 4)
+ {
+ switch (m_command[0])
+ {
+ // 1 byte out
+ case BackupRead:
+ m_backup.ReadBytes(&byte, 1);
+ break;
+ case DMAOffsetLengthSet:
+ byte = 0x01;
+ break;
+ // 2 byte out
+ case ReadISR:
+ if (m_position == 6)
+ {
+ byte = s_irq_status;
+ s_interrupt_set = false;
+ }
+ else
+ {
+ byte = 0x04;
+ }
+ break;
+ // 2 byte out
+ case ReadIMR:
+ if (m_position == 5)
+ byte = 0xFF;
+ if (m_position == 6)
+ byte = 0x81;
+ break;
default:
- ERROR_LOG_FMT(EXPANSIONINTERFACE, "EXI BASEBOARD: Unhandled command {:#x} {:#x}", m_command,
- m_position);
+ ERROR_LOG_FMT(SP1, "Unknown AM-BB command: {:02x}", m_command[0]);
+ break;
}
}
+ else
+ {
+ byte = 0xFF;
+ }
+
+ DEBUG_LOG_FMT(SP1, "AM-BB < {:02x}", byte);
m_position++;
}
void CEXIBaseboard::DoState(PointerWrap& p)
{
p.Do(m_position);
+ p.Do(s_interrupt_set);
p.Do(m_command);
}
+
} // namespace ExpansionInterface
diff --git a/Source/Core/Core/HW/EXI/EXI_DeviceBaseboard.h b/Source/Core/Core/HW/EXI/EXI_DeviceBaseboard.h
index 4c534428c9..be929026e3 100644
--- a/Source/Core/Core/HW/EXI/EXI_DeviceBaseboard.h
+++ b/Source/Core/Core/HW/EXI/EXI_DeviceBaseboard.h
@@ -1,27 +1,59 @@
-// Copyright 2013 Dolphin Emulator Project
+// Copyright 2025 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
+#include "Common/CommonTypes.h"
+#include "Common/IOFile.h"
#include "Core/HW/EXI/EXI_Device.h"
-class PointerWrap;
+namespace Core
+{
+class System;
+}
namespace ExpansionInterface
{
-class CEXIBaseboard : public IEXIDevice
+void GenerateInterrupt(int flag);
+
+class CEXIBaseboard final : public IEXIDevice
{
public:
- CEXIBaseboard(Core::System& system);
+ explicit CEXIBaseboard(Core::System& system);
+ ~CEXIBaseboard() override;
+
void SetCS(int cs) override;
+ bool IsInterruptSet() override;
bool IsPresent() const override;
void DoState(PointerWrap& p) override;
+ void DMAWrite(u32 addr, u32 size) override;
+ void DMARead(u32 addr, u32 size) override;
private:
- // STATE_TO_SAVE
+ enum Command
+ {
+ BackupOffsetSet = 0x01,
+ BackupWrite = 0x02,
+ BackupRead = 0x03,
+
+ DMAOffsetLengthSet = 0x05,
+
+ ReadISR = 0x82,
+ WriteISR = 0x83,
+ ReadIMR = 0x86,
+ WriteIMR = 0x87,
+
+ WriteLANCNT = 0xFF,
+ };
+
u32 m_position = 0;
- u32 m_command = 0;
+ u32 m_backup_dma_offset = 0;
+ u32 m_backup_dma_length = 0;
+ std::array<u8, 4> m_command{};
+ u16 m_backup_offset = 0;
+ File::IOFile m_backup;
+protected:
void TransferByte(u8& byte) override;
};
} // namespace ExpansionInterface
diff --git a/Source/Core/Core/HW/GCPadEmu.cpp b/Source/Core/Core/HW/GCPadEmu.cpp
index 0058bc74e4..dff79653c3 100644
--- a/Source/Core/Core/HW/GCPadEmu.cpp
+++ b/Source/Core/Core/HW/GCPadEmu.cpp
@@ -34,6 +34,7 @@ static const u16 trigger_bitmasks[] = {
static const u16 dpad_bitmasks[] = {PAD_BUTTON_UP, PAD_BUTTON_DOWN, PAD_BUTTON_LEFT,
PAD_BUTTON_RIGHT};
+static const u8 triforce_bitmask[] = {SWITCH_TEST, SWITCH_SERVICE, SWITCH_COIN};
GCPad::GCPad(const unsigned int index) : m_index(index)
{
@@ -68,6 +69,13 @@ GCPad::GCPad(const unsigned int index) : m_index(index)
m_dpad->AddInput(Translatability::Translate, named_direction);
}
+ // triforce
+ groups.emplace_back(m_triforce = new ControllerEmu::Buttons(TRIFORCE_GROUP));
+ for (const char* named_button : {TEST_BUTTON, SERVICE_BUTTON, COIN_BUTTON})
+ {
+ m_triforce->AddInput(Translatability::Translate, named_button);
+ }
+
// Microphone
groups.emplace_back(m_mic = new ControllerEmu::Buttons(MIC_GROUP));
m_mic->AddInput(Translatability::Translate, _trans("Button"));
@@ -119,6 +127,8 @@ ControllerEmu::ControlGroup* GCPad::GetGroup(PadGroup group)
return m_mic;
case PadGroup::Options:
return m_options;
+ case PadGroup::Triforce:
+ return m_triforce;
default:
return nullptr;
}
@@ -150,6 +160,9 @@ GCPadStatus GCPad::GetInput() const
// dpad
m_dpad->GetState(&pad.button, dpad_bitmasks, m_input_override_function);
+ // triforce
+ m_triforce->GetState(&pad.switches, triforce_bitmask, m_input_override_function);
+
// sticks
const auto main_stick_state = m_main_stick->GetState(m_input_override_function);
pad.stickX = MapFloat<u8>(main_stick_state.x, GCPadStatus::MAIN_STICK_CENTER_X, 1);
@@ -181,6 +194,9 @@ void GCPad::LoadDefaults(const ControllerInterface& ciface)
#ifdef ANDROID
// Rumble
m_rumble->SetControlExpression(0, "`Android/0/Device Sensors:Motor 0`");
+
+ // Triforce Coin
+ m_triforce->SetControlExpression(2, "pulse(`Android/0/Device Sensors:Accel Down` > 15, 0.1)");
#else
// Buttons: A, B, X, Y, Z
m_buttons->SetControlExpression(0, "`X`");
@@ -202,6 +218,11 @@ void GCPad::LoadDefaults(const ControllerInterface& ciface)
m_dpad->SetControlExpression(2, "`F`"); // Left
m_dpad->SetControlExpression(3, "`H`"); // Right
+ // Triforce
+ m_triforce->SetControlExpression(0, "`1`"); // Test
+ m_triforce->SetControlExpression(1, "`2`"); // Service
+ m_triforce->SetControlExpression(2, "`3`"); // Coin
+
// C Stick
m_c_stick->SetControlExpression(0, "`I`"); // Up
m_c_stick->SetControlExpression(1, "`K`"); // Down
diff --git a/Source/Core/Core/HW/GCPadEmu.h b/Source/Core/Core/HW/GCPadEmu.h
index 2555ee05b1..906f7750f3 100644
--- a/Source/Core/Core/HW/GCPadEmu.h
+++ b/Source/Core/Core/HW/GCPadEmu.h
@@ -29,7 +29,8 @@ enum class PadGroup
Triggers,
Rumble,
Mic,
- Options
+ Options,
+ Triforce,
};
class GCPad : public ControllerEmu::EmulatedController
@@ -61,6 +62,7 @@ public:
static constexpr const char* RUMBLE_GROUP = _trans("Rumble");
static constexpr const char* MIC_GROUP = _trans("Microphone");
static constexpr const char* OPTIONS_GROUP = _trans("Options");
+ static constexpr const char* TRIFORCE_GROUP = _trans("Triforce");
static constexpr const char* A_BUTTON = "A";
static constexpr const char* B_BUTTON = "B";
@@ -69,6 +71,11 @@ public:
static constexpr const char* Z_BUTTON = "Z";
static constexpr const char* START_BUTTON = "Start";
+ // Special Triforce buttons
+ static constexpr const char* TEST_BUTTON = _trans("Test");
+ static constexpr const char* SERVICE_BUTTON = _trans("Service");
+ static constexpr const char* COIN_BUTTON = _trans("Coin");
+
// i18n: The left trigger button (labeled L on real controllers)
static constexpr const char* L_DIGITAL = _trans("L");
// i18n: The right trigger button (labeled R on real controllers)
@@ -87,6 +94,7 @@ private:
ControllerEmu::ControlGroup* m_rumble;
ControllerEmu::Buttons* m_mic;
ControllerEmu::ControlGroup* m_options;
+ ControllerEmu::Buttons* m_triforce;
ControllerEmu::SettingValue<bool> m_always_connected_setting;
diff --git a/Source/Core/Core/HW/MagCard/C1231BR.cpp b/Source/Core/Core/HW/MagCard/C1231BR.cpp
new file mode 100644
index 0000000000..de76df1839
--- /dev/null
+++ b/Source/Core/Core/HW/MagCard/C1231BR.cpp
@@ -0,0 +1,82 @@
+// Copyright 2026 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+// Based on: https://github.com/GXTX/YACardEmu
+// Copyright (C) 2020-2023 wutno (https://github.com/GXTX)
+// Copyright (C) 2022-2023 tugpoat (https://github.com/tugpoat)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "Core/HW/MagCard/C1231BR.h"
+
+namespace MagCard
+{
+
+void C1231BR::Command_10_Initialize()
+{
+ // TODO: Not hardware tested.
+ if (!m_current_packet.empty())
+ {
+ if (InitMode(m_current_packet[0]) == InitMode::EjectAfter)
+ {
+ m_is_shutter_open = true;
+ MoveCard(R::ENTRY_SLOT);
+ }
+ }
+
+ // m_status.SoftReset(); // TODO: Needed ?
+ FinishCommand();
+}
+
+void C1231BR::Command_D0_ControlShutter()
+{
+ enum class Action : u8
+ {
+ Close = 0x30,
+ Open = 0x31,
+ };
+
+ if (m_current_packet.empty())
+ {
+ SetPError(P::SYSTEM_ERR);
+ FinishCommand();
+ return;
+ }
+
+ m_is_shutter_open = Action(m_current_packet[0]) == Action::Open;
+
+ INFO_LOG_FMT(SERIALINTERFACE_CARD, "Shutter: {}", m_is_shutter_open ? "Open" : "Close");
+
+ FinishCommand();
+}
+
+u8 C1231BR::GetPositionValue()
+{
+ // True "R" (Card Position) values.
+ // FYI: I see a 0xd0 command transition from: 11000 -> 01110 -> 00111
+ // Are these 5 bits individual sensors placed within the machine ?
+ static constexpr std::array<u8, std::size_t(R::MAX_POSITIONS)> POSITION_VALUES{
+ 0b00000, // NO_CARD
+ 0b11000, // READ_WRITE_HEAD
+ 0b00111, // THERMAL_HEAD
+ 0b11100, // DISPENSER_THERMAL
+ 0b00001, // ENTRY_SLOT
+ };
+
+ return (m_is_shutter_open ? 0x80u : 0x40u) |
+ (m_card_settings->report_dispenser_empty ? 0x00u : 0x20u) |
+ POSITION_VALUES[u8(m_status.r)];
+}
+
+void C1231BR::DoState(PointerWrap& p)
+{
+ MagneticCardReader::DoState(p);
+
+ p.Do(m_is_shutter_open);
+}
+
+bool C1231BR::IsReadyForCard()
+{
+ return !IsCardPresent() && m_is_shutter_open;
+}
+
+} // namespace MagCard
diff --git a/Source/Core/Core/HW/MagCard/C1231BR.h b/Source/Core/Core/HW/MagCard/C1231BR.h
new file mode 100644
index 0000000000..d339b2d5bb
--- /dev/null
+++ b/Source/Core/Core/HW/MagCard/C1231BR.h
@@ -0,0 +1,37 @@
+// Copyright 2026 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+// Based on: https://github.com/GXTX/YACardEmu
+// Copyright (C) 2020-2023 wutno (https://github.com/GXTX)
+// Copyright (C) 2022-2023 tugpoat (https://github.com/tugpoat)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "Core/HW/MagCard/MagneticCardReader.h"
+
+namespace MagCard
+{
+
+// CRP-1231BR-10
+// Used by F-Zero AX
+class C1231BR final : public MagneticCardReader
+{
+public:
+ using MagneticCardReader::MagneticCardReader;
+
+protected:
+ // Specific to this model.
+ bool m_is_shutter_open = true;
+
+ bool IsReadyForCard() override;
+
+ u8 GetPositionValue() override;
+
+ void Command_10_Initialize() override;
+ void Command_D0_ControlShutter() override;
+
+ void DoState(PointerWrap& p) override;
+};
+
+} // namespace MagCard
diff --git a/Source/Core/Core/HW/MagCard/C1231LR.cpp b/Source/Core/Core/HW/MagCard/C1231LR.cpp
new file mode 100644
index 0000000000..0e9c32fa15
--- /dev/null
+++ b/Source/Core/Core/HW/MagCard/C1231LR.cpp
@@ -0,0 +1,27 @@
+// Copyright 2026 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+// Based on: https://github.com/GXTX/YACardEmu
+// Copyright (C) 2020-2023 wutno (https://github.com/GXTX)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "Core/HW/MagCard/C1231LR.h"
+
+namespace MagCard
+{
+
+u8 C1231LR::GetPositionValue()
+{
+ // True "R" (Card Position) values.
+ static constexpr std::array<u8, std::size_t(R::MAX_POSITIONS)> POSITION_VALUES{
+ 0x30, // NO_CARD
+ 0x31, // READ_WRITE_HEAD
+ 0x32, // THERMAL_HEAD
+ 0x33, // DISPENSER_THERMAL
+ 0x34, // ENTRY_SLOT
+ };
+
+ return POSITION_VALUES[u8(m_status.r)];
+}
+
+} // namespace MagCard
diff --git a/Source/Core/Core/HW/MagCard/C1231LR.h b/Source/Core/Core/HW/MagCard/C1231LR.h
new file mode 100644
index 0000000000..e7ee627e1b
--- /dev/null
+++ b/Source/Core/Core/HW/MagCard/C1231LR.h
@@ -0,0 +1,26 @@
+// Copyright 2026 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+// Based on: https://github.com/GXTX/YACardEmu
+// Copyright (C) 2020-2023 wutno (https://github.com/GXTX)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "Core/HW/MagCard/MagneticCardReader.h"
+
+namespace MagCard
+{
+
+// CRP-1231LR-10NAB
+// Used by Mario Kart Arcade GP + GP2
+class C1231LR final : public MagneticCardReader
+{
+public:
+ using MagneticCardReader::MagneticCardReader;
+
+protected:
+ u8 GetPositionValue() override;
+};
+
+} // namespace MagCard
diff --git a/Source/Core/Core/HW/MagCard/MagneticCardReader.cpp b/Source/Core/Core/HW/MagCard/MagneticCardReader.cpp
new file mode 100644
index 0000000000..abb93359e1
--- /dev/null
+++ b/Source/Core/Core/HW/MagCard/MagneticCardReader.cpp
@@ -0,0 +1,1000 @@
+// Copyright 2026 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+// Based on: https://github.com/GXTX/YACardEmu
+// Copyright (C) 2020-2023 wutno (https://github.com/GXTX)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "Core/HW/MagCard/MagneticCardReader.h"
+
+#include <algorithm>
+#include <numeric>
+
+#include "Common/BitUtils.h"
+#include "Common/DirectIOFile.h"
+#include "Common/FileUtil.h"
+#include "Common/Lazy.h"
+#include "Common/Logging/Log.h"
+#include "Common/SmallVector.h"
+#include "Common/StringUtil.h"
+#include "Core/Core.h"
+
+namespace
+{
+
+constexpr std::string GetHexDump(const auto& data)
+{
+ const auto u8_span = Common::AsU8Span(data);
+ return HexDump(u8_span.data(), u8_span.size());
+}
+
+void AppendRange(auto* container, const auto& range)
+{
+ const auto prev_size = container->size();
+ container->resize(prev_size + range.size());
+ std::ranges::copy(range, container->data() + prev_size);
+}
+
+} // namespace
+
+namespace MagCard
+{
+
+constexpr std::string_view VERSION_STRING = "AP:S1234-5678,OS:S9012-3456,0000";
+
+constexpr u8 START_OF_TEXT = 0x02;
+constexpr u8 END_OF_TEXT = 0x03;
+constexpr u8 ENQUIRY = 0x05;
+constexpr u8 ACK = 0x06;
+constexpr u8 NACK = 0x15;
+
+enum class BitMode : u8
+{
+ SevenBitParity = 0x30,
+ EightBitNoParity = 0x31,
+};
+
+enum class Track : u8
+{
+ Track_1 = 0x30,
+ Track_2 = 0x31,
+ Track_3 = 0x32,
+ Track_1_And_2 = 0x33,
+ Track_1_And_3 = 0x34,
+ Track_2_And_3 = 0x35,
+ Track_1_2_And_3 = 0x36,
+};
+
+static auto GetTrackIndicesForTrackType(Track track)
+{
+ Common::SmallVector<std::size_t, MagneticCardReader::NUM_TRACKS> result;
+
+ switch (track)
+ {
+ case Track::Track_1:
+ case Track::Track_2:
+ case Track::Track_3:
+ result.emplace_back(std::size_t(track) - 0x30);
+ break;
+
+ case Track::Track_1_And_2:
+ case Track::Track_1_And_3:
+ case Track::Track_2_And_3:
+ result.emplace_back((track == Track::Track_2_And_3) ? 1 : 0);
+ result.emplace_back((track == Track::Track_1_And_2) ? 1 : 2);
+ break;
+
+ case Track::Track_1_2_And_3:
+ result.emplace_back(0);
+ result.emplace_back(1);
+ result.emplace_back(2);
+ break;
+
+ default:
+ break;
+ }
+
+ return result;
+}
+
+MagneticCardReader::MagneticCardReader(MagneticCardReader::Settings* settings)
+ : m_card_settings{settings}
+{
+}
+
+MagneticCardReader::~MagneticCardReader()
+{
+ if (IsCardLoaded())
+ {
+ // Games do seem to write the entire save data in a single command.
+ // But the printed messages are another story.
+ // I suppose there's potential for data loss here.
+ WARN_LOG_FMT(SERIALINTERFACE_CARD, "Shutdown before ejecting card!");
+ }
+}
+
+void MagneticCardReader::Command_10_Initialize()
+{
+ EjectCard();
+ m_status.SoftReset();
+ FinishCommand();
+}
+
+void MagneticCardReader::Command_20_ReadStatus()
+{
+ m_status.SoftReset();
+ FinishCommand();
+}
+
+void MagneticCardReader::Command_33_ReadData()
+{
+ enum class Mode : u8
+ {
+ Standard = 0x30, // read 69-bytes
+ ReadVariable = 0x31, // variable length read
+ CardCapture = 0x32, // pull in card
+ };
+
+ if (m_current_packet.size() < 3)
+ {
+ SetPError(P::SYSTEM_ERR);
+ return;
+ }
+
+ const auto mode = Mode(m_current_packet[0]);
+ // const auto bit_mode = BitMode(m_current_packet[1]);
+ const auto track = Track(m_current_packet[2]);
+
+ switch (m_current_step)
+ {
+ case 1:
+ if (mode == Mode::CardCapture)
+ INFO_LOG_FMT(SERIALINTERFACE_CARD, "Command_33_ReadData: CardCapture");
+ break;
+ case 2:
+ {
+ // Doesn't return card data.
+ if (mode == Mode::CardCapture)
+ {
+ if (!IsCardPresent())
+ {
+ m_status.s = S::WAITING_FOR_CARD;
+ --m_current_step;
+ break;
+ }
+
+ // How does hardware behave when the card is already elsewhere in the machine ?
+ if (m_status.r != R::READ_WRITE_HEAD)
+ {
+ NOTICE_LOG_FMT(SERIALINTERFACE_CARD, "Capturing Card.");
+
+ MoveCard(R::READ_WRITE_HEAD);
+ ReadCardFile();
+
+ Core::DisplayMessage("Inserted Magnetic Card", 4000);
+ }
+ break;
+ }
+
+ DEBUG_LOG_FMT(SERIALINTERFACE_CARD, "Command_33_ReadData: mode {:02x} track {:02x}", u8(mode),
+ u8(track));
+
+ const auto track_indices = GetTrackIndicesForTrackType(track);
+
+ if (!IsCardLoaded() || track_indices.size() == 0)
+ {
+ SetPError(P::ILLEGAL_ERR);
+ return;
+ }
+
+ for (auto track_index : track_indices)
+ {
+ auto& track_data = m_card_data[track_index];
+ if (!track_data.has_value())
+ {
+ SetPError(P::READ_ERR);
+ WARN_LOG_FMT(SERIALINTERFACE_CARD, "Command_33_ReadData: Empty track {:02x}", track_index);
+ // TODO: Are we supposed to return partial data ?
+ return;
+ }
+
+ AppendRange(&m_command_payload, *track_data);
+ }
+ }
+ default:
+ break;
+ }
+
+ if (m_current_step >= 2)
+ FinishCommand();
+}
+
+void MagneticCardReader::Command_35_GetData()
+{
+ switch (m_current_step)
+ {
+ case 2:
+ if (!IsCardLoaded())
+ {
+ // TODO: Is this correct ?
+ SetPError(P::ILLEGAL_ERR);
+ return;
+ }
+
+ for (auto& track_data : m_card_data)
+ {
+ if (!track_data.has_value())
+ {
+ SetPError(P::READ_ERR);
+ // TODO: Are we supposed to return partial data ?
+ return;
+ }
+
+ AppendRange(&m_command_payload, *track_data);
+ }
+ break;
+ default:
+ break;
+ }
+
+ if (m_current_step >= 2)
+ FinishCommand();
+}
+
+void MagneticCardReader::Command_40_Cancel()
+{
+ m_status.SoftReset();
+ FinishCommand();
+}
+
+void MagneticCardReader::Command_53_WriteData()
+{
+ enum class Mode : u8
+ {
+ Standard = 0x30, // 69-bytes
+ };
+
+ if (m_current_packet.size() < 3)
+ {
+ SetPError(P::SYSTEM_ERR);
+ return;
+ }
+
+ if (m_current_step < 2)
+ return;
+
+ const auto mode = Mode(m_current_packet[0]);
+ // const auto bit_mode = BitMode(m_current_packet[1]);
+ auto track = Track(m_current_packet[2]);
+
+ const auto track_indices = GetTrackIndicesForTrackType(track);
+ const auto proper_payload_size = track_indices.size() * TRACK_SIZE;
+ const auto payload = std::span{m_current_packet}.subspan(3);
+
+ if (mode != Mode::Standard || !IsCardLoaded() || track_indices.size() == 0 ||
+ payload.size() != proper_payload_size)
+ {
+ WARN_LOG_FMT(SERIALINTERFACE_CARD, "Command_53_WriteData: Failed.");
+ SetPError(P::ILLEGAL_ERR);
+ return;
+ }
+
+ INFO_LOG_FMT(SERIALINTERFACE_CARD, "Command_53_WriteData: Writing {} track(s) to memory.",
+ track_indices.size());
+
+ for (auto track_index : track_indices)
+ {
+ auto& track_data = m_card_data[track_index];
+ track_data.emplace();
+ std::ranges::copy(payload.subspan(track_index * TRACK_SIZE, TRACK_SIZE), track_data->data());
+ }
+
+ Core::DisplayMessage("Writing Magnetic Card", 4000);
+
+ WriteCardFile();
+
+ FinishCommand();
+}
+
+void MagneticCardReader::Command_78_PrintSettings()
+{
+ m_status.SoftReset();
+ FinishCommand();
+}
+
+void MagneticCardReader::Command_7A_RegisterFont()
+{
+ m_status.SoftReset();
+ FinishCommand();
+}
+
+void MagneticCardReader::Command_7B_PrintImage()
+{
+ // TODO: Are these print functions supposed to succeed before the card is actually grabbed ?
+ if (!IsCardPresent())
+ {
+ SetPError(P::PRINT_ERR);
+ return;
+ }
+ m_status.SoftReset();
+ FinishCommand();
+}
+
+void MagneticCardReader::Command_7C_PrintLine()
+{
+ if (m_current_packet.size() < 3)
+ {
+ SetPError(P::SYSTEM_ERR);
+ return;
+ }
+
+ switch (m_current_step)
+ {
+ case 1:
+ {
+ if (!IsCardPresent())
+ {
+ SetPError(P::PRINT_ERR);
+ return;
+ }
+
+ // FIXME: Should we only move the head when we're actually about to print?
+ MoveCard(R::THERMAL_HEAD);
+ break;
+ }
+ case 2:
+ MoveCard(R::READ_WRITE_HEAD);
+ break;
+ default:
+ break;
+ }
+
+ if (m_current_step >= 2)
+ FinishCommand();
+}
+
+void MagneticCardReader::Command_7D_Erase()
+{
+ switch (m_current_step)
+ {
+ case 1:
+ if (!IsCardPresent())
+ {
+ SetPError(P::PRINT_ERR);
+ return;
+ }
+ MoveCard(R::THERMAL_HEAD);
+ break;
+ case 2:
+ MoveCard(R::READ_WRITE_HEAD);
+ break;
+ default:
+ break;
+ }
+
+ if (m_current_step >= 2)
+ FinishCommand();
+}
+
+void MagneticCardReader::Command_7E_PrintBarcode()
+{
+ switch (m_current_step)
+ {
+ case 2:
+ if (!IsCardPresent())
+ {
+ SetPError(P::ILLEGAL_ERR);
+ return;
+ }
+ break;
+ default:
+ break;
+ }
+
+ if (m_current_step >= 2)
+ FinishCommand();
+}
+
+void MagneticCardReader::Command_80_EjectCard()
+{
+ if (m_current_step >= 3)
+ {
+ EjectCard();
+ FinishCommand();
+ }
+}
+
+void MagneticCardReader::Command_A0_Clean()
+{
+ switch (m_current_step)
+ {
+ case 2:
+ // TODO: How is this command supposed to behave if a card is already in the machine ?
+ if (!IsCardLoaded())
+ {
+ NOTICE_LOG_FMT(SERIALINTERFACE_CARD, "Inserting cleaning card.");
+ Core::DisplayMessage("Inserting cleaning card", 4000);
+ MoveCard(R::THERMAL_HEAD);
+ }
+ break;
+ case 3:
+ EjectCard();
+ break;
+ default:
+ break;
+ }
+
+ if (m_current_step >= 3)
+ FinishCommand();
+}
+
+void MagneticCardReader::Command_B0_DispenseCard()
+{
+ enum class Mode : u8
+ {
+ Dispense = 0x31,
+ CheckOnly = 0x32,
+ };
+
+ // MarioKart GP1 issues this command without options.
+ auto mode = Mode::Dispense;
+ if (!m_current_packet.empty())
+ {
+ mode = Mode(m_current_packet[0]);
+ }
+
+ switch (m_current_step)
+ {
+ case 2:
+ if (mode == Mode::CheckOnly)
+ {
+ if (m_card_settings->report_dispenser_empty)
+ {
+ m_status.s = S::DISPENSER_EMPTY;
+ }
+ else
+ {
+ m_status.s = S::CARD_FULL;
+ }
+ }
+ else
+ {
+ DispenseCard();
+ }
+ break;
+ case 3:
+ if (m_status.r == R::DISPENSER_THERMAL)
+ {
+ MoveCard(R::READ_WRITE_HEAD);
+ }
+ break;
+ default:
+ break;
+ }
+
+ if (m_current_step >= 3)
+ FinishCommand();
+}
+
+void MagneticCardReader::Command_C0_ControlLED()
+{
+ // We don't need to handle this properly but let's leave some notes.
+ enum class Mode : u8
+ {
+ Off = 0x30,
+ On = 0x31,
+ SlowBlink = 0x32,
+ FastBlink = 0x33,
+ };
+
+ m_status.SoftReset();
+ FinishCommand();
+}
+
+void MagneticCardReader::Command_C1_SetPrintRetry()
+{
+ // We don't need to handle this properly but let's leave some notes
+ // m_current_packet[0] == 0x31 NONE ~ 0x39 MAX8
+ m_status.SoftReset();
+ FinishCommand();
+}
+
+void MagneticCardReader::Command_D0_ControlShutter()
+{
+ // Only BR model supports this command.
+ SetSError(S::ILLEGAL_COMMAND);
+}
+
+void MagneticCardReader::Command_F0_GetVersion()
+{
+ AppendRange(&m_command_payload, VERSION_STRING);
+
+ m_status.SoftReset();
+ FinishCommand();
+}
+
+void MagneticCardReader::ClearCardInMemory()
+{
+ m_card_data.fill(std::nullopt);
+}
+
+void MagneticCardReader::ReadCardFile()
+{
+ NOTICE_LOG_FMT(SERIALINTERFACE_CARD, "Reading card data from: {}", m_card_settings->card_name);
+
+ ClearCardInMemory();
+
+ const std::string full_path = m_card_settings->card_path + m_card_settings->card_name;
+
+ File::DirectIOFile file{full_path, File::AccessMode::Read};
+
+ const auto file_size = file.GetSize();
+ const auto complete_track_count = file_size / TRACK_SIZE;
+ const auto excess_byte_count = file_size % TRACK_SIZE;
+
+ if (file_size == 0 || excess_byte_count != 0 || complete_track_count > NUM_TRACKS)
+ {
+ ERROR_LOG_FMT(SERIALINTERFACE_CARD, "ReadCardFile: Incorrect card file size: {}", file_size);
+ }
+
+ const auto tracks_to_read = std::min<u32>(complete_track_count, NUM_TRACKS);
+ for (u32 track_num = 0; track_num != tracks_to_read; ++track_num)
+ {
+ auto& track_data = m_card_data[track_num];
+ track_data.emplace();
+
+ if (!file.Read(*track_data))
+ {
+ ERROR_LOG_FMT(SERIALINTERFACE_CARD, "File read failed.");
+ track_data.reset();
+ break;
+ }
+ }
+
+ DEBUG_LOG_FMT(SERIALINTERFACE_CARD, "ReadCard: 0:{} 1:{} 2:{}", GetHexDump(m_card_data[0]),
+ GetHexDump(m_card_data[1]), GetHexDump(m_card_data[2]));
+}
+
+void MagneticCardReader::WriteCardFile()
+{
+ // Note: Creating empty upsets games when reinserting them later.
+ // Games expect to dispense their own blank cards, not have such cards inserted.
+ // We only write files when the game itself writes, so this won't happen.
+
+ NOTICE_LOG_FMT(SERIALINTERFACE_CARD, "Writing card data to: {}", m_card_settings->card_name);
+ DEBUG_LOG_FMT(SERIALINTERFACE_CARD, "WriteCard: 0:{} 1:{} 2:{}", GetHexDump(m_card_data[0]),
+ GetHexDump(m_card_data[1]), GetHexDump(m_card_data[2]));
+
+ auto full_path = m_card_settings->card_path + m_card_settings->card_name;
+
+ File::DirectIOFile file{full_path, File::AccessMode::Write};
+
+ for (auto& track_data : m_card_data)
+ {
+ if (track_data.has_value() && !file.Write(*track_data))
+ {
+ ERROR_LOG_FMT(SERIALINTERFACE_CARD, "File write failed.");
+ break;
+ }
+ }
+}
+
+void MagneticCardReader::SetPError(P error_code)
+{
+ WARN_LOG_FMT(SERIALINTERFACE_CARD, "SetPError: {:02x}", u8(error_code));
+ m_status.p = error_code;
+ FinishCommand();
+}
+
+void MagneticCardReader::SetSError(S error_code)
+{
+ WARN_LOG_FMT(SERIALINTERFACE_CARD, "SetSError: {:02x}", u8(error_code));
+ m_status.s = error_code;
+ FinishCommand();
+}
+
+void MagneticCardReader::Process(std::vector<u8>* read, std::vector<u8>* write)
+{
+ while (!read->empty())
+ {
+ const u8 first_byte = read->front();
+ if (first_byte == ENQUIRY)
+ {
+ // ENQUIRY
+ // The host sends this regularly.
+ // This triggers sending of the latest command buffer.
+ // We also step the state machine here.
+
+ DEBUG_LOG_FMT(SERIALINTERFACE_CARD, "Process: ENQ");
+
+ // TODO: Get time from Core rather than assuming.
+ const auto elapsed_time = std::chrono::milliseconds{50};
+
+ StepStateMachine(elapsed_time);
+ StepStatePerson(elapsed_time);
+
+ BuildPacket(*write);
+
+ read->erase(read->begin()); // TODO: SLOW !
+ continue;
+ }
+
+ if (first_byte != START_OF_TEXT)
+ {
+ ERROR_LOG_FMT(SERIALINTERFACE_CARD, "Process: Unexpected {:02x}", first_byte);
+ read->erase(read->begin()); // TODO: SLOW !
+ continue;
+ }
+
+ // START_OF_TEXT
+ // This is a command packet. The next byte provides the size.
+ // Upon read they ACK or NACK and start processing of a command.
+
+ if (read->size() < 2)
+ break; // Wait for more data.
+
+ const std::size_t packet_size = (*read)[1];
+ if (packet_size > read->size() - 2)
+ break; // Wait for more data.
+
+ if (ReceivePacket(std::span{*read}.subspan(2, packet_size)))
+ {
+ DEBUG_LOG_FMT(SERIALINTERFACE_CARD, "Writing ACK");
+ write->emplace_back(ACK);
+ }
+ else
+ {
+ WARN_LOG_FMT(SERIALINTERFACE_CARD, "Writing NACK");
+ write->emplace_back(NACK);
+ }
+
+ read->erase(read->begin(), read->begin() + packet_size + 2); // TODO: SLOW !
+ }
+}
+
+bool MagneticCardReader::ReceivePacket(std::span<const u8> packet)
+{
+ DEBUG_LOG_FMT(SERIALINTERFACE_CARD, "ReceivePacket: {}", GetHexDump(packet));
+
+ if (packet.size() < 6)
+ {
+ WARN_LOG_FMT(SERIALINTERFACE_CARD, "ReceivePacket: Bad packet size!");
+ return false;
+ }
+
+ if (packet[packet.size() - 2] != END_OF_TEXT)
+ {
+ WARN_LOG_FMT(SERIALINTERFACE_CARD, "ReceivePacket: Missing END_OF_TEXT!");
+ return false;
+ }
+
+ // Checksum is XOR of the previous bytes.
+ // This includes the count byte that we already chopped off.
+ const u8 read_checksum = packet.back();
+
+ // TODO C++23:
+ // std::ranges::fold_left(packet.first(packet.size() - 1), u8(packet.size()), std::bit_xor{});
+ const auto range_to_checksum = packet.first(packet.size() - 1);
+ const auto proper_checksum = std::accumulate(range_to_checksum.begin(), range_to_checksum.end(),
+ u8(packet.size()), std::bit_xor{});
+
+ // Verify checksum. Skip packet if invalid.
+ if (read_checksum != proper_checksum)
+ {
+ WARN_LOG_FMT(SERIALINTERFACE_CARD, "ReceivePacket: Bad checksum!");
+ return false;
+ }
+
+ const u8 new_command = packet[0];
+
+ if (IsRunningCommand())
+ {
+ if (new_command == 0x40) // Cancel.
+ {
+ INFO_LOG_FMT(SERIALINTERFACE_CARD, "ReceivePacket: Cancelling command: {:02x}",
+ m_current_command);
+ }
+ else
+ {
+ // What does the real hardware do here ?
+ WARN_LOG_FMT(SERIALINTERFACE_CARD,
+ "ReceivePacket: Running command: {:02x} interrupted by command: {:02x}",
+ m_current_command, new_command);
+ }
+ }
+
+ m_current_command = new_command;
+
+ // Copy the payload.
+ // We're skipping the command number and the host's 3 status bytes,
+ // and dropping the END_OF_TEXT and checksum.
+ m_current_packet.resize(packet.size() - 6);
+ std::ranges::copy(packet.subspan(4, m_current_packet.size()), m_current_packet.data());
+
+ // TODO: Is this sensible for every command ?
+ m_status.SoftReset();
+
+ // Start the new command.
+ m_status.s = S::RUNNING_COMMAND;
+ m_current_step = 1;
+ m_command_payload.clear();
+
+ return true;
+}
+
+void MagneticCardReader::StepStatePerson(DT elapsed_time)
+{
+ m_time_since_person_moved_card += elapsed_time;
+
+ // F-Zero AX: Polls for a card with command 0x20 ReadStatus().
+ // It demands a card is removed from "ENTRY_SLOT" during boot.
+ // It pauses and spams ReadStatus() until that happens.
+ // When wanted, it loads a card detected at the "ENTRY_SLOT" position.
+ // There's not a perfectly reliably way to detect when it wants a card.
+ //
+ // MarioKart GP: Polls for wanted cards with command 0x33 ReadData(Mode:CardCapture).
+ // It does this regardless of the card being detected in the "ENTRY_SLOT" position.
+ // This makes it easy to detect when cards are wanted.
+ // It pauses until cards are pulled from "ENTRY_SLOT" on game over.
+
+ // This logic tries to emulate a person following those rules.
+
+ // People can't interact with cards inside the machine.
+ if (IsCardLoaded())
+ return;
+
+ Common::Lazy<bool> card_exists{[&] {
+ const auto full_path = m_card_settings->card_path + m_card_settings->card_name;
+ return File::Exists(full_path);
+ }};
+
+ if (!IsCardPresent())
+ {
+ if (m_card_settings->insert_card_when_possible && IsReadyForCard() &&
+ m_time_since_person_moved_card > std::chrono::milliseconds{500} && *card_exists)
+ {
+ // Place the card in the entry slot after a delay.
+ NOTICE_LOG_FMT(SERIALINTERFACE_CARD, "Inserting card.");
+ m_status.r = R::ENTRY_SLOT;
+ m_time_since_person_moved_card = {};
+ }
+
+ return;
+ }
+
+ const bool machine_moved_card_last =
+ m_time_since_machine_moved_card < m_time_since_person_moved_card;
+ if ((machine_moved_card_last &&
+ m_time_since_machine_moved_card > std::chrono::milliseconds{100}) ||
+ !*card_exists)
+ {
+ // Take the card from the machine when ejected or if the file is deleted.
+ NOTICE_LOG_FMT(SERIALINTERFACE_CARD, "Removing card.");
+ m_status.r = R::NO_CARD;
+ m_time_since_person_moved_card = {};
+ }
+}
+
+void MagneticCardReader::StepStateMachine(DT elapsed_time)
+{
+ m_time_since_machine_moved_card += elapsed_time;
+
+ if (!IsRunningCommand())
+ return;
+
+ DEBUG_LOG_FMT(SERIALINTERFACE_CARD, "StepStateMachine command: {:02x}", GetCurrentCommand());
+
+ switch (GetCurrentCommand())
+ {
+ case 0x10:
+ Command_10_Initialize();
+ break;
+ case 0x20:
+ Command_20_ReadStatus();
+ break;
+ case 0x33:
+ Command_33_ReadData();
+ break;
+ case 0x35:
+ Command_35_GetData();
+ break;
+ case 0x40:
+ Command_40_Cancel();
+ break;
+ case 0x53:
+ Command_53_WriteData();
+ break;
+ case 0x78:
+ Command_78_PrintSettings();
+ break;
+ case 0x7A:
+ Command_7A_RegisterFont();
+ break;
+ case 0x7B:
+ Command_7B_PrintImage();
+ break;
+ case 0x7C:
+ Command_7C_PrintLine();
+ break;
+ case 0x7D:
+ Command_7D_Erase();
+ break;
+ case 0x7E:
+ Command_7E_PrintBarcode();
+ break;
+ case 0x80:
+ Command_80_EjectCard();
+ break;
+ case 0xA0:
+ Command_A0_Clean();
+ break;
+ case 0xB0:
+ Command_B0_DispenseCard();
+ break;
+ case 0xC0:
+ Command_C0_ControlLED();
+ break;
+ case 0xC1:
+ Command_C1_SetPrintRetry();
+ break;
+ case 0xD0:
+ Command_D0_ControlShutter();
+ break;
+ case 0xF0:
+ Command_F0_GetVersion();
+ break;
+ default:
+ WARN_LOG_FMT(SERIALINTERFACE_CARD, "HandlePacket: Unhandled command {:02x}",
+ GetCurrentCommand());
+ SetSError(S::ILLEGAL_COMMAND);
+ break;
+ }
+
+ if (IsRunningCommand())
+ ++m_current_step;
+}
+
+void MagneticCardReader::BuildPacket(std::vector<u8>& write_buffer)
+{
+ // Header and footer add 6 bytes.
+ const u8 payload_size = u8(m_command_payload.size() + 6);
+ // + START_OF_TEXT + the count byte
+ const auto total_write_size = payload_size + 2;
+
+ const auto prev_buffer_size = write_buffer.size();
+ write_buffer.resize(prev_buffer_size + total_write_size);
+
+ auto* out_ptr = write_buffer.data() + prev_buffer_size;
+ u8 packet_checksum = 0;
+
+ const auto write_and_checksum = [&](u8 value) {
+ *(out_ptr++) = value;
+ packet_checksum ^= value;
+ };
+
+ // Write the header.
+ *(out_ptr++) = START_OF_TEXT;
+ write_and_checksum(payload_size);
+ write_and_checksum(GetCurrentCommand());
+ write_and_checksum(GetPositionValue());
+ write_and_checksum(u8(m_status.p));
+ write_and_checksum(u8(m_status.s));
+
+ // Write the payload.
+ std::ranges::for_each(m_command_payload, write_and_checksum);
+
+ // Write the footer.
+ write_and_checksum(END_OF_TEXT);
+ *(out_ptr++) = packet_checksum;
+
+ DEBUG_LOG_FMT(
+ SERIALINTERFACE_CARD, "BuildPacket: {}",
+ GetHexDump(std::span{write_buffer}.subspan(write_buffer.size() - payload_size - 2)));
+}
+
+bool MagneticCardReader::IsRunningCommand() const
+{
+ return m_current_step > 0;
+}
+
+void MagneticCardReader::FinishCommand()
+{
+ m_current_step = 0;
+
+ if (m_status.s != S::ILLEGAL_COMMAND)
+ m_status.s = S::NO_JOB;
+}
+
+u8 MagneticCardReader::GetCurrentCommand() const
+{
+ return m_current_command;
+}
+
+bool MagneticCardReader::IsCardPresent() const
+{
+ return m_status.r != R::NO_CARD;
+}
+
+bool MagneticCardReader::IsCardLoaded() const
+{
+ return m_status.r != R::NO_CARD && m_status.r != R::ENTRY_SLOT;
+}
+
+void MagneticCardReader::MoveCard(R new_position)
+{
+ if (m_status.r == new_position)
+ return;
+
+ static constexpr const char* position_descriptions[] = {
+ "NO_CARD", "READ_WRITE_HEAD", "THERMAL_HEAD", "DISPENSER_THERMAL", "ENTRY_SLOT",
+ };
+
+ INFO_LOG_FMT(SERIALINTERFACE_CARD, "MoveCard: {}",
+ position_descriptions[std::size_t(new_position)]);
+
+ m_status.r = new_position;
+ m_time_since_machine_moved_card = {};
+}
+
+void MagneticCardReader::EjectCard()
+{
+ if (IsCardLoaded())
+ {
+ NOTICE_LOG_FMT(SERIALINTERFACE_CARD, "Ejecting card.");
+
+ ClearCardInMemory();
+ MoveCard(R::ENTRY_SLOT);
+
+ Core::DisplayMessage("Ejected Magnetic Card", 4000);
+ }
+}
+
+void MagneticCardReader::DispenseCard()
+{
+ // TODO: Think about how multiple card management UX should work.
+ // Since this is a new card, we maybe shouldn't be overwriting existing files.
+
+ if (IsCardLoaded())
+ {
+ SetSError(S::ILLEGAL_COMMAND);
+ return;
+ }
+
+ if (m_card_settings->report_dispenser_empty)
+ {
+ m_status.s = S::DISPENSER_EMPTY;
+ return;
+ }
+
+ NOTICE_LOG_FMT(SERIALINTERFACE_CARD, "Dispensing a new card.");
+ Core::DisplayMessage("Created Magnetic Card", 4000);
+
+ // TODO: This behavior doesn't seem to match protocol dumps.
+ MoveCard(R::DISPENSER_THERMAL);
+}
+
+bool MagneticCardReader::IsReadyForCard()
+{
+ return !IsCardPresent();
+}
+
+void MagneticCardReader::DoState(PointerWrap& p)
+{
+ // Outgoing packet.
+ p.Do(m_status);
+ p.Do(m_current_command);
+ p.Do(m_command_payload);
+
+ // Command state.
+ p.Do(m_current_packet);
+ p.Do(m_current_step);
+
+ // The card data in memory.
+ for (auto& track : m_card_data)
+ p.Do(track);
+
+ p.Do(m_time_since_machine_moved_card);
+ p.Do(m_time_since_person_moved_card);
+}
+
+} // namespace MagCard
diff --git a/Source/Core/Core/HW/MagCard/MagneticCardReader.h b/Source/Core/Core/HW/MagCard/MagneticCardReader.h
new file mode 100644
index 0000000000..b60514a6ab
--- /dev/null
+++ b/Source/Core/Core/HW/MagCard/MagneticCardReader.h
@@ -0,0 +1,207 @@
+// Copyright 2026 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+// Based on: https://github.com/GXTX/YACardEmu
+// Copyright (C) 2020-2023 wutno (https://github.com/GXTX)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <array>
+#include <optional>
+#include <span>
+#include <string>
+#include <vector>
+
+#include "Common/ChunkFile.h"
+#include "Common/CommonTypes.h"
+
+namespace MagCard
+{
+
+class MagneticCardReader
+{
+public:
+ static constexpr std::size_t TRACK_SIZE = 69; // A nice amount of data.
+ static constexpr std::size_t NUM_TRACKS = 3;
+
+ // This data is not part of the card reader state.
+ struct Settings
+ {
+ std::string card_name;
+ std::string card_path;
+
+ bool insert_card_when_possible = true;
+ bool report_dispenser_empty = false;
+ };
+
+ explicit MagneticCardReader(Settings* settings);
+ virtual ~MagneticCardReader();
+
+ MagneticCardReader(const MagneticCardReader&) = delete;
+ MagneticCardReader& operator=(const MagneticCardReader&) = delete;
+ MagneticCardReader(MagneticCardReader&&) = delete;
+ MagneticCardReader& operator=(MagneticCardReader&&) = delete;
+
+ // TODO: This std:vector buffer interface is a bit funky..
+
+ // read = MagCard <-- Baseboard.
+ // write = Magcard --> Baseboard.
+ void Process(std::vector<u8>* read, std::vector<u8>* write);
+
+ virtual void DoState(PointerWrap& p);
+
+protected:
+ // Status bytes:
+ enum class R : u8
+ {
+ // Note: Derived classes define the real values in GetPositionValue().
+ NO_CARD,
+ READ_WRITE_HEAD,
+ THERMAL_HEAD,
+ DISPENSER_THERMAL,
+ ENTRY_SLOT,
+
+ MAX_POSITIONS,
+ };
+ enum class P : u8
+ {
+ NO_ERR = 0x30,
+ READ_ERR = 0x31,
+ WRITE_ERR = 0x32,
+ CARD_JAM = 0x33,
+ MOTOR_ERR = 0x34, // transport system motor error
+ PRINT_ERR = 0x35,
+ ILLEGAL_ERR = 0x38, // generic error
+ BATTERY_ERR = 0x40, // low battery voltage
+ SYSTEM_ERR = 0x41, // reader/writer system err
+ TRACK_1_READ_ERR = 0x51,
+ TRACK_2_READ_ERR = 0x52,
+ TRACK_3_READ_ERR = 0x53,
+ TRACK_1_AND_2_READ_ERR = 0x54,
+ TRACK_1_AND_3_READ_ERR = 0x55,
+ TRACK_2_AND_3_READ_ERR = 0x56,
+ };
+ enum class S : u8
+ {
+ NO_JOB = 0x30,
+ ILLEGAL_COMMAND = 0x32,
+ RUNNING_COMMAND = 0x33,
+ WAITING_FOR_CARD = 0x34,
+ DISPENSER_EMPTY = 0x35,
+ NO_DISPENSER = 0x36,
+ CARD_FULL = 0x37,
+ };
+
+ struct Status
+ {
+ R r = R::NO_CARD;
+ P p = P::NO_ERR;
+ S s = S::NO_JOB;
+
+ void SoftReset()
+ {
+ p = P::NO_ERR;
+ s = S::NO_JOB;
+ }
+ };
+
+ enum class InitMode : u8
+ {
+ Standard = 0x30,
+ EjectAfter = 0x31,
+ ResetSpecifications = 0x32,
+ };
+
+ bool ReceivePacket(std::span<const u8> packet);
+ void BuildPacket(std::vector<u8>& write_buffer);
+
+ void StepStateMachine(DT elapsed_time);
+ void StepStatePerson(DT elapsed_time);
+
+ // This is what actually maps the "R" position value to a number.
+ virtual u8 GetPositionValue() = 0;
+
+ void SetPError(P error_code);
+ void SetSError(S error_code);
+
+ void ClearCardInMemory();
+
+ // Read/Write with host filesystem.
+ void ReadCardFile();
+ void WriteCardFile();
+
+ bool IsRunningCommand() const;
+ void FinishCommand();
+
+ u8 GetCurrentCommand() const;
+
+ // Is a card anywhere the machine can sense it ?
+ bool IsCardPresent() const;
+
+ // Is a card beyond the "ENTRY_SLOT" position ?
+ bool IsCardLoaded() const;
+
+ virtual bool IsReadyForCard();
+
+ // Move the card through the machine.
+ void MoveCard(R new_position);
+
+ // Unloads the card and moves it to the "ENTRY_SLOT" position.
+ void EjectCard();
+
+ // Feeds a new card into the machine from an internal hopper.
+ void DispenseCard();
+
+ // Commands
+ virtual void Command_10_Initialize();
+ void Command_20_ReadStatus();
+ void Command_33_ReadData(); // Multi-track read.
+ void Command_35_GetData(); // Read the entire card.
+ void Command_40_Cancel();
+ void Command_53_WriteData(); // Multi-track write.
+ void Command_78_PrintSettings();
+ void Command_7A_RegisterFont();
+ void Command_7B_PrintImage();
+ void Command_7C_PrintLine();
+ void Command_7D_Erase(); // Erase the printed image.
+ void Command_7E_PrintBarcode();
+ void Command_80_EjectCard();
+ void Command_A0_Clean();
+ void Command_B0_DispenseCard();
+ void Command_C0_ControlLED();
+ void Command_C1_SetPrintRetry();
+ virtual void Command_D0_ControlShutter();
+ void Command_F0_GetVersion();
+
+ Settings* const m_card_settings;
+
+ // R P S values that are sent in every ENQUIRY response.
+ Status m_status{};
+
+ // A byte sent in every ENQUIRY response.
+ // TODO: What's the default value on just powered up hardware ?
+ u8 m_current_command = 0;
+
+ // The data included in every ENQUIRY response.
+ std::vector<u8> m_command_payload;
+
+ // The card track data in memory.
+ using TrackData = std::optional<std::array<u8, TRACK_SIZE>>;
+ std::array<TrackData, NUM_TRACKS> m_card_data;
+
+ // The parameter data of the command being processed.
+ std::vector<u8> m_current_packet;
+
+ // The progress state of the current command.
+ // 0 == not running a command.
+ // TODO: Change this to emulated milliseconds or something.
+ s32 m_current_step = 0;
+
+ // Used to delay re-insertion of cards after ejection.
+ // This avoids having to expose card "Take"/"Insert" actions.
+ DT m_time_since_machine_moved_card{};
+ DT m_time_since_person_moved_card{};
+};
+
+} // namespace MagCard
diff --git a/Source/Core/Core/HW/Memmap.h b/Source/Core/Core/HW/Memmap.h
index ce45d73034..4b33776a62 100644
--- a/Source/Core/Core/HW/Memmap.h
+++ b/Source/Core/Core/HW/Memmap.h
@@ -123,7 +123,6 @@ public:
// If the specified range is within a single valid memory region, returns a pointer to the start
// of the corresponding range in host memory. Otherwise, returns nullptr.
u8* GetPointerForRange(u32 address, size_t size) const;
-
void CopyFromEmu(void* data, u32 address, size_t size) const;
void CopyToEmu(u32 address, const void* data, size_t size);
void Memset(u32 address, u8 value, size_t size);
diff --git a/Source/Core/Core/HW/SI/SI.h b/Source/Core/Core/HW/SI/SI.h
index 731b9b5c19..5f8a47afd7 100644
--- a/Source/Core/Core/HW/SI/SI.h
+++ b/Source/Core/Core/HW/SI/SI.h
@@ -56,6 +56,7 @@ public:
void RemoveEvent(int device_number);
void UpdateDevices();
+ u32 GetInLength() const { return m_com_csr.INLNGTH; }
void RemoveDevice(int device_number);
void AddDevice(SIDevices device, int device_number);
diff --git a/Source/Core/Core/HW/SI/SI_Device.cpp b/Source/Core/Core/HW/SI/SI_Device.cpp
index 451523624b..8614bb156c 100644
--- a/Source/Core/Core/HW/SI/SI_Device.cpp
+++ b/Source/Core/Core/HW/SI/SI_Device.cpp
@@ -17,6 +17,7 @@
#ifdef HAS_LIBMGBA
#include "Core/HW/SI/SI_DeviceGBAEmu.h"
#endif
+#include "Core/HW/SI/SI_DeviceAMBaseboard.h"
#include "Core/HW/SI/SI_DeviceGCAdapter.h"
#include "Core/HW/SI/SI_DeviceGCController.h"
#include "Core/HW/SI/SI_DeviceGCSteeringWheel.h"
@@ -149,6 +150,7 @@ bool SIDevice_IsGCController(SIDevices type)
case SIDEVICE_GC_TARUKONGA:
case SIDEVICE_DANCEMAT:
case SIDEVICE_GC_STEERING:
+ case SIDEVICE_AM_BASEBOARD:
return true;
default:
return false;
@@ -191,6 +193,8 @@ std::unique_ptr<ISIDevice> SIDevice_Create(Core::System& system, const SIDevices
return std::make_unique<CSIDevice_Keyboard>(system, device, port_number);
case SIDEVICE_AM_BASEBOARD:
+ return std::make_unique<CSIDevice_AMBaseboard>(system, device, port_number);
+
case SIDEVICE_NONE:
default:
return std::make_unique<CSIDevice_Null>(system, device, port_number);
diff --git a/Source/Core/Core/HW/SI/SI_Device.h b/Source/Core/Core/HW/SI/SI_Device.h
index 234c475e5f..c3171f42e8 100644
--- a/Source/Core/Core/HW/SI/SI_Device.h
+++ b/Source/Core/Core/HW/SI/SI_Device.h
@@ -97,8 +97,6 @@ enum SIDevices : int
SIDEVICE_GC_STEERING,
SIDEVICE_DANCEMAT,
SIDEVICE_GC_TARUKONGA,
- // Was used for Triforce in the past, but the implementation is no longer in Dolphin.
- // It's kept here so that values below will stay constant.
SIDEVICE_AM_BASEBOARD,
SIDEVICE_WIIU_ADAPTER,
SIDEVICE_GC_GBA_EMULATED,
diff --git a/Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.cpp b/Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.cpp
new file mode 100644
index 0000000000..5532c06b87
--- /dev/null
+++ b/Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.cpp
@@ -0,0 +1,2353 @@
+// Copyright 2025 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "Core/HW/SI/SI_DeviceAMBaseboard.h"
+
+#include <algorithm>
+#include <numeric>
+#include <string>
+
+#include <fmt/format.h>
+
+#include "Common/CommonTypes.h"
+#include "Common/FileUtil.h"
+#include "Common/Logging/Log.h"
+#include "Common/MsgHandler.h"
+#include "Common/Swap.h"
+
+#include "Core/ConfigManager.h"
+#include "Core/Core.h"
+#include "Core/CoreTiming.h"
+#include "Core/HW/DVD/AMMediaboard.h"
+#include "Core/HW/GCPad.h"
+#include "Core/HW/MagCard/C1231BR.h"
+#include "Core/HW/MagCard/C1231LR.h"
+#include "Core/HW/Memmap.h"
+#include "Core/HW/ProcessorInterface.h"
+#include "Core/HW/SI/SI.h"
+#include "Core/HW/SI/SI_Device.h"
+#include "Core/HW/SI/SI_DeviceGCController.h"
+#include "Core/HW/SystemTimers.h"
+#include "Core/Movie.h"
+#include "Core/NetPlayProto.h"
+#include "Core/System.h"
+
+#include "InputCommon/GCPadStatus.h"
+
+namespace SerialInterface
+{
+using namespace AMMediaboard;
+
+void JVSIOMessage::Start(int node)
+{
+ m_last_start = m_pointer;
+ const u8 header[3] = {0xE0, (u8)node, 0};
+ m_checksum = 0;
+ AddData(header, 3, 1);
+}
+
+void JVSIOMessage::AddData(const u8* dst, std::size_t len, int sync = 0)
+{
+ if (m_pointer + len >= sizeof(m_message))
+ {
+ PanicAlertFmt("JVSIOMessage overrun!");
+ return;
+ }
+
+ while (len--)
+ {
+ const u8 c = *dst++;
+ if (!sync && ((c == 0xE0) || (c == 0xD0)))
+ {
+ if (m_pointer + 2 > sizeof(m_message))
+ {
+ PanicAlertFmt("JVSIOMessage overrun!");
+ break;
+ }
+ m_message[m_pointer++] = 0xD0;
+ m_message[m_pointer++] = c - 1;
+ }
+ else
+ {
+ if (m_pointer >= sizeof(m_message))
+ {
+ PanicAlertFmt("JVSIOMessage overrun!");
+ break;
+ }
+ m_message[m_pointer++] = c;
+ }
+
+ if (!sync)
+ m_checksum += c;
+ sync = 0;
+ }
+}
+
+void JVSIOMessage::AddData(const void* data, std::size_t len)
+{
+ AddData(static_cast<const u8*>(data), len);
+}
+
+void JVSIOMessage::AddData(const char* data)
+{
+ AddData(data, strlen(data));
+}
+
+void JVSIOMessage::AddData(u32 n)
+{
+ const u8 cs = n;
+ AddData(&cs, 1);
+}
+
+void JVSIOMessage::End()
+{
+ const u32 len = m_pointer - m_last_start;
+ if (m_last_start + 2 < sizeof(m_message) && len >= 3)
+ {
+ m_message[m_last_start + 2] = len - 2; // assuming len <0xD0
+ AddData(m_checksum + len - 2);
+ }
+ else
+ {
+ PanicAlertFmt("JVSIOMessage: Not enough space for checksum!");
+ }
+}
+
+static constexpr u8 CheckSumXOR(const u8* data, u32 length)
+{
+ return std::accumulate(data, data + length, u8{}, std::bit_xor());
+}
+
+static constexpr char s_cdr_program_version[] = {" Version 1.22,2003/09/19,171-8213B"};
+static constexpr char s_cdr_boot_version[] = {" Version 1.04,2003/06/17,171-8213B"};
+static constexpr u8 s_cdr_card_data[] = {
+ 0x00, 0x6E, 0x00, 0x00, 0x01, 0x00, 0x00, 0x06, 0x00, 0x00, 0x07, 0x00, 0x00, 0x0B,
+ 0x00, 0x00, 0x0E, 0x00, 0x00, 0x10, 0x00, 0x00, 0x17, 0x00, 0x00, 0x19, 0x00, 0x00,
+ 0x1A, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x1D, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x20, 0x00,
+ 0x00, 0x22, 0x00, 0x00, 0x23, 0x00, 0x00, 0x24, 0x00, 0x00, 0x27, 0x00, 0x00, 0x28,
+ 0x00, 0x00, 0x2C, 0x00, 0x00, 0x2F, 0x00, 0x00, 0x34, 0x00, 0x00, 0x35, 0x00, 0x00,
+ 0x37, 0x00, 0x00, 0x38, 0x00, 0x00, 0x39, 0x00, 0x00, 0x3D, 0x00};
+
+const constexpr u8 s_region_flags[] = "\x00\x00\x30\x00"
+ // "\x01\xfe\x00\x00" // JAPAN
+ "\x02\xfd\x00\x00" // USA
+ //"\x03\xfc\x00\x00" // export
+ "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff";
+// AM-Baseboard device on SI
+CSIDevice_AMBaseboard::CSIDevice_AMBaseboard(Core::System& system, SIDevices device,
+ int device_number)
+ : ISIDevice(system, device, device_number)
+{
+ // Card ID
+ m_ic_card_data[0x20] = 0x95;
+ m_ic_card_data[0x21] = 0x71;
+
+ if (AMMediaboard::GetGameType() == KeyOfAvalon)
+ {
+ m_ic_card_data[0x22] = 0x26;
+ m_ic_card_data[0x23] = 0x40;
+ }
+ else if (AMMediaboard::GetGameType() == VirtuaStriker4)
+ {
+ m_ic_card_data[0x22] = 0x44;
+ m_ic_card_data[0x23] = 0x00;
+ }
+
+ // Use count
+ m_ic_card_data[0x28] = 0xFF;
+ m_ic_card_data[0x29] = 0xFF;
+
+ // Magnetic Card Reader
+ m_mag_card_settings.card_path = File::GetUserPath(D_TRIUSER_IDX);
+ m_mag_card_settings.card_name = fmt::format("tricard_{}.bin", SConfig::GetInstance().GetGameID());
+
+ // TODO: Do any other games use the Magnetic Card Reader ?
+ switch (AMMediaboard::GetGameType())
+ {
+ case FZeroAX:
+ m_mag_card_reader = std::make_unique<MagCard::C1231BR>(&m_mag_card_settings);
+ break;
+
+ case MarioKartGP:
+ case MarioKartGP2:
+ m_mag_card_reader = std::make_unique<MagCard::C1231LR>(&m_mag_card_settings);
+ break;
+
+ default:
+ break;
+ }
+}
+
+constexpr u32 SI_XFER_LENGTH_MASK = 0x7f;
+
+// Translate [0,1,2,...,126,127] to [128,1,2,...,126,127]
+constexpr s32 ConvertSILengthField(u32 field)
+{
+ return ((field - 1) & SI_XFER_LENGTH_MASK) + 1;
+}
+
+void CSIDevice_AMBaseboard::ICCardSendReply(ICCommand* iccommand, u8* buffer, u32* length)
+{
+ iccommand->status = Common::swap16(iccommand->status);
+
+ const auto iccommand_data = reinterpret_cast<const u8*>(iccommand);
+ const u8 crc = CheckSumXOR(iccommand_data + 2, iccommand->pktlen - 1);
+
+ for (u32 i = 0; i <= iccommand->pktlen; ++i)
+ {
+ buffer[(*length)++] = iccommand_data[i];
+ }
+
+ buffer[(*length)++] = crc;
+}
+
+void CSIDevice_AMBaseboard::SwapBuffers(u8* buffer, u32* buffer_length)
+{
+ memcpy(m_last[1], buffer, 0x80); // Save current buffer
+ memcpy(buffer, m_last[0], 0x80); // Load previous buffer
+ memcpy(m_last[0], m_last[1], 0x80); // Update history
+
+ m_lastptr[1] = *buffer_length; // Swap lengths
+ *buffer_length = m_lastptr[0];
+ m_lastptr[0] = m_lastptr[1];
+}
+
+int CSIDevice_AMBaseboard::RunBuffer(u8* buffer, int request_length)
+{
+ const auto& serial_interface = m_system.GetSerialInterface();
+ u32 buffer_length = ConvertSILengthField(serial_interface.GetInLength());
+
+ // Debug logging
+ ISIDevice::RunBuffer(buffer, buffer_length);
+
+ u32 buffer_position = 0;
+ while (buffer_position < buffer_length)
+ {
+ const auto bb_command = static_cast<BaseBoardCommand>(buffer[buffer_position]);
+ buffer_position++;
+
+ switch (bb_command)
+ {
+ case BaseBoardCommand::GCAM_Reset: // Returns ID and dip switches
+ {
+ const u32 id = Common::swap32(SI_AM_BASEBOARD | 0x100);
+ std::memcpy(buffer, &id, sizeof(id));
+ return sizeof(id);
+ }
+ break;
+ case BaseBoardCommand::GCAM_Command:
+ {
+ u32 checksum = 0;
+ for (u32 i = 0; i < buffer_length; ++i)
+ checksum += buffer[i];
+
+ std::array<u8, 0x80> data_out{};
+ u32 data_offset = 0;
+
+ data_out[data_offset++] = 1;
+ data_out[data_offset++] = 1;
+
+ u8* data_in = buffer + 2;
+ if (buffer_position >= buffer_length)
+ {
+ ERROR_LOG_FMT(SERIALINTERFACE_AMBB, "GC-AM: buffer overflow (position={}, length={})",
+ buffer_position, buffer_length);
+ buffer_position = buffer_length;
+ break;
+ }
+
+ const u32 requested_size = buffer[buffer_position] + 2;
+ if (requested_size > buffer_length)
+ {
+ ERROR_LOG_FMT(SERIALINTERFACE_AMBB, "GC-AM: requested size ({}) bigger than buffer's ({})",
+ requested_size, buffer_length);
+ buffer_position = buffer_length;
+ break;
+ }
+ u8* const data_in_end = buffer + requested_size;
+
+ // Helper to check that iterating over data n times is safe,
+ // i.e. *data++ at most lead to data.end()
+ auto validate_data_in_out = [&](u32 n_in, u32 n_out, std::string_view command) -> bool {
+ if (data_in + n_in > data_in_end)
+ ERROR_LOG_FMT(SERIALINTERFACE_AMBB, "GC-AM: data_in overflow in {}", command);
+ else if (u64{data_offset} + n_out > data_out.size())
+ ERROR_LOG_FMT(SERIALINTERFACE_AMBB, "GC-AM: data_out overflow in {}", command);
+ else
+ return true;
+ ERROR_LOG_FMT(SERIALINTERFACE_AMBB,
+ "Overflow details:\n"
+ " - data_in(begin={}, current={}, end={}, n_in={})\n"
+ " - data_out(offset={}, size={}, n_out={})\n"
+ " - buffer(position={}, length={})",
+ fmt::ptr(buffer + 2), fmt::ptr(data_in), fmt::ptr(data_in_end), n_in,
+ data_offset, data_out.size(), n_out, buffer_position, buffer_length);
+ data_in = data_in_end;
+ return false;
+ };
+
+ while (data_in < data_in_end)
+ {
+ const u32 gcam_command = *data_in++;
+ switch (GCAMCommand(gcam_command))
+ {
+ case GCAMCommand::StatusSwitches:
+ {
+ if (!validate_data_in_out(1, 4, "StatusSwitches"))
+ break;
+
+ const u8 status = *data_in++;
+ DEBUG_LOG_FMT(SERIALINTERFACE_AMBB, "GC-AM: Command 0x10, {:02x} (READ STATUS&SWITCHES)",
+ status);
+
+ data_out[data_offset++] = gcam_command;
+ data_out[data_offset++] = 0x2;
+
+ // We read Test/Service from the JVS-I/O SwitchesInput instead
+ //
+ // const GCPadStatus pad_status = Pad::GetStatus(ISIDevice::m_device_number);
+ // baseboard test/service switches
+ // if (pad_status.button & PAD_BUTTON_Y) // Test
+ // dip_switch_0 &= ~0x80;
+ // if (pad_status.button & PAD_BUTTON_X) // Service
+ // dip_switch_0 &= ~0x40;
+
+ // Horizontal Scanning Frequency switch
+ // Required for F-Zero AX booting via Sega Boot
+ if (AMMediaboard::GetGameType() == FZeroAX ||
+ AMMediaboard::GetGameType() == FZeroAXMonster)
+ {
+ m_dip_switch_0 &= ~0x20;
+ }
+
+ // Disable camera in MKGP1/2
+ if (AMMediaboard::GetGameType() == MarioKartGP ||
+ AMMediaboard::GetGameType() == MarioKartGP2)
+ {
+ m_dip_switch_0 &= ~0x10;
+ }
+
+ data_out[data_offset++] = m_dip_switch_0;
+ data_out[data_offset++] = m_dip_switch_1;
+ break;
+ }
+ case GCAMCommand::SerialNumber:
+ {
+ if (!validate_data_in_out(1, 18, "SerialNumber"))
+ break;
+
+ NOTICE_LOG_FMT(SERIALINTERFACE_AMBB, "GC-AM: Command 0x11, {:02x} (READ SERIAL NR)",
+ *data_in);
+ data_in++;
+
+ data_out[data_offset++] = gcam_command;
+ data_out[data_offset++] = 16;
+ memcpy(data_out.data() + data_offset, "AADE-01B98394904", 16);
+
+ data_offset += 16;
+ break;
+ }
+ case GCAMCommand::Unknown_12:
+ if (!validate_data_in_out(2, 2, "Unknown_12"))
+ break;
+
+ NOTICE_LOG_FMT(SERIALINTERFACE_AMBB, "GC-AM: Command 0x12, {:02x} {:02x}", data_in[0],
+ data_in[1]);
+
+ data_out[data_offset++] = gcam_command;
+ data_out[data_offset++] = 0x00;
+
+ data_in += 2;
+ break;
+ case GCAMCommand::Unknown_14:
+ if (!validate_data_in_out(2, 2, "Unknown_14"))
+ break;
+
+ NOTICE_LOG_FMT(SERIALINTERFACE_AMBB, "GC-AM: Command 0x14, {:02x} {:02x}", data_in[0],
+ data_in[1]);
+
+ data_out[data_offset++] = gcam_command;
+ data_out[data_offset++] = 0x00;
+
+ data_in += 2;
+ break;
+ case GCAMCommand::FirmVersion:
+ if (!validate_data_in_out(1, 4, "FirmVersion"))
+ break;
+
+ NOTICE_LOG_FMT(SERIALINTERFACE_AMBB, "GC-AM: Command 0x15, {:02x} (READ FIRM VERSION)",
+ *data_in);
+ data_in++;
+
+ data_out[data_offset++] = gcam_command;
+ data_out[data_offset++] = 0x02;
+ // 00.26
+ data_out[data_offset++] = 0x00;
+ data_out[data_offset++] = 0x26;
+ break;
+ case GCAMCommand::FPGAVersion:
+ if (!validate_data_in_out(1, 4, "FPGAVersion"))
+ break;
+
+ NOTICE_LOG_FMT(SERIALINTERFACE_AMBB, "GC-AM: Command 0x16, {:02x} (READ FPGA VERSION)",
+ *data_in);
+ data_in++;
+
+ data_out[data_offset++] = gcam_command;
+ data_out[data_offset++] = 0x02;
+ // 07.06
+ data_out[data_offset++] = 0x07;
+ data_out[data_offset++] = 0x06;
+ break;
+ case GCAMCommand::RegionSettings:
+ {
+ if (!validate_data_in_out(5, 0x16, "RegionSettings"))
+ break;
+
+ // Used by SegaBoot for region checks (dev mode skips this check)
+ // In some games this also controls the displayed language
+ NOTICE_LOG_FMT(SERIALINTERFACE_AMBB,
+ "GC-AM: Command 0x1F, {:02x} {:02x} {:02x} {:02x} {:02x} (REGION)",
+ data_in[0], data_in[1], data_in[2], data_in[3], data_in[4]);
+
+ data_out[data_offset++] = gcam_command;
+ data_out[data_offset++] = 0x14;
+
+ for (int i = 0; i < 0x14; ++i)
+ data_out[data_offset++] = s_region_flags[i];
+
+ data_in += 5;
+ }
+ break;
+ // No reply
+ // Note: Always sends three bytes even though size is set to two
+ case GCAMCommand::Unknown_21:
+ {
+ if (!validate_data_in_out(4, 0, "Unknown_21"))
+ break;
+
+ DEBUG_LOG_FMT(SERIALINTERFACE_AMBB, "GC-AM: Command 0x21, {:02x}, {:02x}, {:02x}, {:02x}",
+ data_in[0], data_in[1], data_in[2], data_in[3]);
+ data_in += 4;
+ }
+ break;
+ // No reply
+ // Note: Always sends six bytes
+ case GCAMCommand::Unknown_22:
+ {
+ if (!validate_data_in_out(7, 0, "Unknown_22"))
+ break;
+
+ DEBUG_LOG_FMT(
+ SERIALINTERFACE_AMBB,
+ "GC-AM: Command 0x22, {:02x}, {:02x}, {:02x}, {:02x}, {:02x}, {:02x}, {:02x}",
+ data_in[0], data_in[1], data_in[2], data_in[3], data_in[4], data_in[5], data_in[6]);
+
+ const u32 in_size = data_in[0] + 1;
+ if (!validate_data_in_out(in_size, 0, "Unknown_22"))
+ break;
+ data_in += in_size;
+ }
+ break;
+ case GCAMCommand::Unknown_23:
+ if (!validate_data_in_out(2, 2, "Unknown_23"))
+ break;
+
+ DEBUG_LOG_FMT(SERIALINTERFACE_AMBB, "GC-AM: Command 0x23, {:02x} {:02x}", data_in[0],
+ data_in[1]);
+
+ data_out[data_offset++] = gcam_command;
+ data_out[data_offset++] = 0x00;
+
+ data_in += 2;
+ break;
+ case GCAMCommand::Unknown_24:
+ if (!validate_data_in_out(2, 2, "Unknown_24"))
+ break;
+
+ DEBUG_LOG_FMT(SERIALINTERFACE_AMBB, "GC-AM: Command 0x24, {:02x} {:02x}", data_in[0],
+ data_in[1]);
+
+ data_out[data_offset++] = gcam_command;
+ data_out[data_offset++] = 0x00;
+
+ data_in += 2;
+ break;
+ case GCAMCommand::SerialA:
+ {
+ if (!validate_data_in_out(1, 0, "SerialA"))
+ break;
+
+ const u32 length = *data_in++;
+ if (length)
+ {
+ if (!validate_data_in_out(length, 0, "SerialA"))
+ break;
+
+ INFO_LOG_FMT(SERIALINTERFACE_AMBB, "GC-AM: Command 0x31, length=0x{:02x}, hexdump:\n{}",
+ length, HexDump(data_in, length));
+
+ // Serial - Wheel
+ if (AMMediaboard::GetGameType() == MarioKartGP ||
+ AMMediaboard::GetGameType() == MarioKartGP2)
+ {
+ if (!validate_data_in_out(10, 2, "SerialA (Wheel)"))
+ break;
+
+ INFO_LOG_FMT(SERIALINTERFACE_AMBB,
+ "GC-AM: Command 0x31, (WHEEL) {:02x}{:02x} {:02x}{:02x} {:02x} {:02x} "
+ "{:02x} {:02x} {:02x} {:02x}",
+ data_in[0], data_in[1], data_in[2], data_in[3], data_in[4], data_in[5],
+ data_in[6], data_in[7], data_in[8], data_in[9]);
+
+ data_out[data_offset++] = gcam_command;
+ data_out[data_offset++] = 0x03;
+
+ switch (m_wheel_init)
+ {
+ case 0:
+ if (!validate_data_in_out(0, 3, "SerialA (Wheel)"))
+ break;
+ data_out[data_offset++] = 'E'; // Error
+ data_out[data_offset++] = '0';
+ data_out[data_offset++] = '0';
+ m_wheel_init++;
+ break;
+ case 1:
+ if (!validate_data_in_out(0, 3, "SerialA (Wheel)"))
+ break;
+ data_out[data_offset++] = 'C'; // Power Off
+ data_out[data_offset++] = '0';
+ data_out[data_offset++] = '6';
+ // Only turn on when a wheel is connected
+ if (serial_interface.GetDeviceType(1) == SerialInterface::SIDEVICE_GC_STEERING)
+ {
+ m_wheel_init++;
+ }
+ break;
+ case 2:
+ if (!validate_data_in_out(0, 3, "SerialA (Wheel)"))
+ break;
+ data_out[data_offset++] = 'C'; // Power On
+ data_out[data_offset++] = '0';
+ data_out[data_offset++] = '1';
+ break;
+ default:
+ break;
+ }
+
+ // u16 CenteringForce= ptr(6);
+ // u16 FrictionForce = ptr(8);
+ // u16 Roll = ptr(10);
+ if (!validate_data_in_out(length, 0, "SerialA (Wheel)"))
+ break;
+ data_in += length;
+ break;
+ }
+
+ // Serial - Unknown
+ if (AMMediaboard::GetGameType() == GekitouProYakyuu)
+ {
+ if (!validate_data_in_out(sizeof(u32), 0, "SerialA (Unknown)"))
+ break;
+ const u32 serial_command = Common::BitCastPtr<u32>(data_in);
+
+ if (serial_command == 0x00001000)
+ {
+ if (!validate_data_in_out(0, 5, "SerialA (Unknown)"))
+ break;
+ data_out[data_offset++] = gcam_command;
+ data_out[data_offset++] = 0x03;
+ data_out[data_offset++] = 1;
+ data_out[data_offset++] = 2;
+ data_out[data_offset++] = 3;
+ }
+
+ if (!validate_data_in_out(length, 0, "SerialA (Unknown)"))
+ break;
+ data_in += length;
+ break;
+ }
+
+ // Serial IC-CARD / Serial Deck Reader
+ if (AMMediaboard::GetGameType() == VirtuaStriker4 ||
+ AMMediaboard::GetGameType() == VirtuaStriker4_2006 ||
+ AMMediaboard::GetGameType() == KeyOfAvalon)
+ {
+ if (!validate_data_in_out(2, 0, "SerialA (IC-CARD)"))
+ break;
+ u32 serial_command = data_in[1];
+
+ ICCommand icco;
+
+ // Set default reply
+ icco.pktcmd = gcam_command;
+ icco.pktlen = 7;
+ icco.fixed = 0x10;
+ icco.command = serial_command;
+ icco.flag = 0;
+ icco.length = 2;
+ icco.status = 0;
+ icco.extlen = 0;
+
+ // Check for rest of data from the write pages command
+ if (m_ic_write_size && m_ic_write_offset)
+ {
+ const u32 size = data_in[1];
+
+ if (!validate_data_in_out(size + 2, 0, "SerialA (IC-CARD)"))
+ break;
+ DEBUG_LOG_FMT(SERIALINTERFACE_CARD, "Command: {}", HexDump(data_in, size + 2));
+
+ INFO_LOG_FMT(
+ SERIALINTERFACE_CARD,
+ "GC-AM: Command 25 (IC-CARD) Write Pages: Off:{:x} Size:{:x} PSize:{:x}",
+ m_ic_write_offset, m_ic_write_size, size);
+
+ if (u64{m_ic_write_offset} + size > sizeof(m_ic_write_buffer))
+ {
+ ERROR_LOG_FMT(SERIALINTERFACE_CARD,
+ "GC-AM: Command 25 (IC-CARD) m_ic_write_buffer overflow:\n"
+ " - m_ic_write_buffer(offset={}, size={})\n"
+ " - size={}\n",
+ m_ic_write_offset, sizeof(m_ic_write_buffer), size);
+ data_in = data_in_end;
+ break;
+ }
+ memcpy(m_ic_write_buffer + m_ic_write_offset, data_in + 2, size);
+
+ m_ic_write_offset += size;
+
+ if (m_ic_write_offset > m_ic_write_size)
+ {
+ m_ic_write_offset = 0;
+
+ const u16 page = m_ic_write_buffer[5];
+ const u16 count = m_ic_write_buffer[7];
+ const u32 write_size = u32(count) * 8;
+ const u32 write_offset = u32(page) * 8;
+
+ if ((write_size + write_offset) > sizeof(m_ic_card_data) ||
+ (10 + write_size) > sizeof(m_ic_write_buffer))
+ {
+ ERROR_LOG_FMT(SERIALINTERFACE_CARD,
+ "GC-AM: Command 25 (IC-CARD) Write Pages overflow:\n"
+ " - m_ic_card_data(offset={}, size={})\n"
+ " - m_ic_write_buffer(offset={}, size={})\n"
+ " - size={}, page={}, count={}\n",
+ write_offset, sizeof(m_ic_card_data), 10,
+ sizeof(m_ic_write_buffer), write_size, page, count);
+ data_in = data_in_end;
+ break;
+ }
+ memcpy(m_ic_card_data + write_offset, m_ic_write_buffer + 10, write_size);
+
+ INFO_LOG_FMT(SERIALINTERFACE_CARD,
+ "GC-AM: Command 25 (IC-CARD) Write Pages:{} Count:{}({:x})", page,
+ count, size);
+
+ icco.command = WritePages;
+
+ if (!validate_data_in_out(0, icco.pktlen + 2, "SerialA (IC-CARD)"))
+ break;
+ ICCardSendReply(&icco, data_out.data(), &data_offset);
+ }
+ if (!validate_data_in_out(length, 0, "SerialA (IC-CARD)"))
+ break;
+ data_in += length;
+ break;
+ }
+
+ switch (ICCARDCommand(serial_command))
+ {
+ case ICCARDCommand::GetStatus:
+ icco.status = m_ic_card_state;
+
+ INFO_LOG_FMT(SERIALINTERFACE_CARD,
+ "GC-AM: Command 0x31 (IC-CARD) Get Status:{:02x}", m_ic_card_state);
+ break;
+ case ICCARDCommand::SetBaudrate:
+ INFO_LOG_FMT(SERIALINTERFACE_CARD, "GC-AM: Command 0x31 (IC-CARD) Set Baudrate");
+ break;
+ case ICCARDCommand::FieldOn:
+ m_ic_card_state |= 0x10;
+ INFO_LOG_FMT(SERIALINTERFACE_CARD, "GC-AM: Command 0x31 (IC-CARD) Field On");
+ break;
+ case ICCARDCommand::InsertCheck:
+ icco.status = m_ic_card_status;
+ INFO_LOG_FMT(SERIALINTERFACE_CARD,
+ "GC-AM: Command 0x31 (IC-CARD) Insert Check:{:02x}", m_ic_card_status);
+ break;
+ case ICCARDCommand::AntiCollision:
+ icco.extlen = 8;
+ icco.length += icco.extlen;
+ icco.pktlen += icco.extlen;
+
+ // Card ID
+ icco.extdata[0] = 0x00;
+ icco.extdata[1] = 0x00;
+ icco.extdata[2] = 0x54;
+ icco.extdata[3] = 0x4D;
+ icco.extdata[4] = 0x50;
+ icco.extdata[5] = 0x00;
+ icco.extdata[6] = 0x00;
+ icco.extdata[7] = 0x00;
+
+ INFO_LOG_FMT(SERIALINTERFACE_CARD, "GC-AM: Command 0x31 (IC-CARD) Anti Collision");
+ break;
+ case ICCARDCommand::SelectCard:
+ icco.extlen = 8;
+ icco.length += icco.extlen;
+ icco.pktlen += icco.extlen;
+
+ // Session
+ icco.extdata[0] = 0x00;
+ icco.extdata[1] = m_ic_card_session;
+ icco.extdata[2] = 0x00;
+ icco.extdata[3] = 0x00;
+ icco.extdata[4] = 0x00;
+ icco.extdata[5] = 0x00;
+ icco.extdata[6] = 0x00;
+ icco.extdata[7] = 0x00;
+
+ INFO_LOG_FMT(SERIALINTERFACE_CARD, "GC-AM: Command 0x31 (IC-CARD) Select Card:{}",
+ m_ic_card_session);
+ break;
+ case ICCARDCommand::ReadPage:
+ case ICCARDCommand::ReadUseCount:
+ {
+ if (!validate_data_in_out(8, 0, "SerialA (IC-CARD)"))
+ break;
+ const u16 page = Common::swap16(data_in + 6) & 0xFF; // 255 is max page
+
+ icco.extlen = 8;
+ icco.length += icco.extlen;
+ icco.pktlen += icco.extlen;
+
+ memcpy(icco.extdata, m_ic_card_data + page * 8, 8);
+
+ INFO_LOG_FMT(SERIALINTERFACE_CARD, "GC-AM: Command 31 (IC-CARD) Read Page:{}",
+ page);
+ break;
+ }
+ case ICCARDCommand::WritePage:
+ {
+ if (!validate_data_in_out(10, 0, "SerialA (IC-CARD)"))
+ break;
+ const u16 page = Common::swap16(data_in + 8) & 0xFF; // 255 is max page
+
+ // Write only one page
+ if (page == 4)
+ {
+ icco.status = 0x80;
+ }
+ else
+ {
+ if (!validate_data_in_out(18, 0, "SerialA (IC-CARD)"))
+ break;
+ memcpy(m_ic_card_data + page * 8, data_in + 10, 8);
+ }
+
+ INFO_LOG_FMT(SERIALINTERFACE_CARD, "GC-AM: Command 0x31 (IC-CARD) Write Page:{}",
+ page);
+ break;
+ }
+ case ICCARDCommand::DecreaseUseCount:
+ {
+ if (!validate_data_in_out(8, 0, "SerialA (IC-CARD)"))
+ break;
+ const u16 page = Common::swap16(data_in + 6) & 0xFF; // 255 is max page
+
+ icco.extlen = 2;
+ icco.length += icco.extlen;
+ icco.pktlen += icco.extlen;
+
+ auto ic_card_data = Common::BitCastPtr<u16>(m_ic_card_data + 0x28);
+ ic_card_data = ic_card_data - 1;
+
+ // Counter
+ icco.extdata[0] = m_ic_card_data[0x28];
+ icco.extdata[1] = m_ic_card_data[0x29];
+
+ INFO_LOG_FMT(SERIALINTERFACE_CARD,
+ "GC-AM: Command 31 (IC-CARD) Decrease Use Count:{}", page);
+ break;
+ }
+ case ICCARDCommand::ReadPages:
+ {
+ if (!validate_data_in_out(10, 0, "SerialA (IC-CARD)"))
+ break;
+ const u16 page = Common::swap16(data_in + 6) & 0xFF; // 255 is max page
+ const u16 count = Common::swap16(data_in + 8);
+
+ const u32 offs = page * 8;
+ u32 cnt = count * 8;
+
+ // Limit read size to not overwrite the reply buffer
+ const std::size_t reply_buffer_size = sizeof(icco.extdata) - 1;
+ if (data_offset > reply_buffer_size)
+ {
+ ERROR_LOG_FMT(SERIALINTERFACE_CARD,
+ "GC-AM: Command 31 (IC-CARD) Read Pages overflow:"
+ " offset={} > buffer_size={}",
+ data_offset, reply_buffer_size);
+ data_in = data_in_end;
+ break;
+ }
+ if (cnt > reply_buffer_size - data_offset)
+ {
+ cnt = 5 * 8;
+ }
+
+ icco.extlen = cnt;
+ icco.length += icco.extlen;
+ icco.pktlen += icco.extlen;
+
+ if (offs + cnt > sizeof(icco.extdata))
+ {
+ ERROR_LOG_FMT(SERIALINTERFACE_CARD,
+ "GC-AM: Command 31 (IC-CARD) Read Pages overflow:"
+ " offset={} + count={} > buffer_size={}",
+ offs, cnt, sizeof(icco.extdata));
+ data_in = data_in_end;
+ break;
+ }
+ memcpy(icco.extdata, m_ic_card_data + offs, cnt);
+
+ INFO_LOG_FMT(SERIALINTERFACE_CARD,
+ "GC-AM: Command 31 (IC-CARD) Read Pages:{} Count:{}", page, count);
+ break;
+ }
+ case ICCARDCommand::WritePages:
+ {
+ if (!validate_data_in_out(10, 0, "SerialA (IC-CARD)"))
+ break;
+ const u16 pksize = length;
+ const u16 size = Common::swap16(data_in + 2);
+ const u16 page = Common::swap16(data_in + 6) & 0xFF; // 255 is max page
+ const u16 count = Common::swap16(data_in + 8);
+ const u32 write_size = u32(count) * 8;
+ const u32 write_offset = u32(page) * 8;
+
+ // We got a complete packet
+ if (pksize - 5 == size)
+ {
+ if (page == 4) // Read Only Page, must return error
+ {
+ icco.status = 0x80;
+ }
+ else
+ {
+ if (write_size + write_offset > sizeof(m_ic_card_data))
+ {
+ ERROR_LOG_FMT(
+ SERIALINTERFACE_CARD,
+ "GC-AM: Command 0x31 (IC-CARD) Data overflow: Pages:{} Count:{}({:x})",
+ page, count, size);
+ }
+ else
+ {
+ if (!validate_data_in_out(13 + write_size, 0, "SerialA (IC-CARD)"))
+ break;
+ memcpy(m_ic_card_data + write_offset, data_in + 13, write_size);
+ }
+ }
+
+ INFO_LOG_FMT(SERIALINTERFACE_CARD,
+ "GC-AM: Command 0x31 (IC-CARD) Write Pages:{} Count:{}({:x})", page,
+ count, size);
+ }
+ // VirtuaStriker 4 splits the writes over multiple packets
+ else
+ {
+ if (!validate_data_in_out(2 + pksize, 0, "SerialA (IC-CARD)"))
+ break;
+ memcpy(m_ic_write_buffer, data_in + 2, pksize);
+ m_ic_write_offset += pksize;
+ m_ic_write_size = size;
+ }
+ break;
+ }
+ default:
+ // Handle Deck Reader commands
+ if (!validate_data_in_out(1, 0, "SerialA (DECK READER)"))
+ break;
+ serial_command = data_in[0];
+ icco.command = serial_command;
+ icco.flag = 0;
+ switch (CDReaderCommand(serial_command))
+ {
+ case CDReaderCommand::ProgramVersion:
+ INFO_LOG_FMT(SERIALINTERFACE_CARD,
+ "GC-AM: Command 0x31 (DECK READER) Program Version");
+
+ icco.extlen = (u32)strlen(s_cdr_program_version);
+ icco.length += icco.extlen;
+ icco.pktlen += icco.extlen;
+
+ memcpy(icco.extdata, s_cdr_program_version, icco.extlen);
+ break;
+ case CDReaderCommand::BootVersion:
+ INFO_LOG_FMT(SERIALINTERFACE_CARD,
+ "GC-AM: Command 0x31 (DECK READER) Boot Version");
+
+ icco.extlen = (u32)strlen(s_cdr_boot_version);
+ icco.length += icco.extlen;
+ icco.pktlen += icco.extlen;
+
+ memcpy(icco.extdata, s_cdr_boot_version, icco.extlen);
+ break;
+ case CDReaderCommand::ShutterGet:
+ INFO_LOG_FMT(SERIALINTERFACE_CARD,
+ "GC-AM: Command 0x31 (DECK READER) Shutter Get");
+
+ icco.extlen = 4;
+ icco.length += icco.extlen;
+ icco.pktlen += icco.extlen;
+
+ icco.extdata[0] = 0;
+ icco.extdata[1] = 0;
+ icco.extdata[2] = 0;
+ icco.extdata[3] = 0;
+ break;
+ case CDReaderCommand::CameraCheck:
+ INFO_LOG_FMT(SERIALINTERFACE_CARD,
+ "GC-AM: Command 0x31 (DECK READER) Camera Check");
+
+ icco.extlen = 6;
+ icco.length += icco.extlen;
+ icco.pktlen += icco.extlen;
+
+ icco.extdata[0] = 0x23;
+ icco.extdata[1] = 0x28;
+ icco.extdata[2] = 0x45;
+ icco.extdata[3] = 0x29;
+ icco.extdata[4] = 0x45;
+ icco.extdata[5] = 0x29;
+ break;
+ case CDReaderCommand::ProgramChecksum:
+ INFO_LOG_FMT(SERIALINTERFACE_CARD,
+ "GC-AM: Command 0x31 (DECK READER) Program Checksum");
+
+ icco.extlen = 4;
+ icco.length += icco.extlen;
+ icco.pktlen += icco.extlen;
+
+ icco.extdata[0] = 0x23;
+ icco.extdata[1] = 0x28;
+ icco.extdata[2] = 0x45;
+ icco.extdata[3] = 0x29;
+ break;
+ case CDReaderCommand::BootChecksum:
+ INFO_LOG_FMT(SERIALINTERFACE_CARD,
+ "GC-AM: Command 0x31 (DECK READER) Boot Checksum");
+
+ icco.extlen = 4;
+ icco.length += icco.extlen;
+ icco.pktlen += icco.extlen;
+
+ icco.extdata[0] = 0x23;
+ icco.extdata[1] = 0x28;
+ icco.extdata[2] = 0x45;
+ icco.extdata[3] = 0x29;
+ break;
+ case CDReaderCommand::SelfTest:
+ INFO_LOG_FMT(SERIALINTERFACE_CARD, "GC-AM: Command 0x31 (DECK READER) Self Test");
+ icco.flag = 0x00;
+ break;
+ case CDReaderCommand::SensLock:
+ INFO_LOG_FMT(SERIALINTERFACE_CARD, "GC-AM: Command 0x31 (DECK READER) Sens Lock");
+ icco.flag = 0x01;
+ break;
+ case CDReaderCommand::SensCard:
+ INFO_LOG_FMT(SERIALINTERFACE_CARD, "GC-AM: Command 0x31 (DECK READER) Sens Card");
+ break;
+ case CDReaderCommand::ShutterCard:
+ INFO_LOG_FMT(SERIALINTERFACE_CARD,
+ "GC-AM: Command 0x31 (DECK READER) Shutter Card");
+ break;
+ case CDReaderCommand::ReadCard:
+ INFO_LOG_FMT(SERIALINTERFACE_CARD, "GC-AM: Command 0x31 (DECK READER) Read Card");
+
+ icco.fixed = 0xAA;
+ icco.flag = 0xAA;
+ icco.extlen = sizeof(s_cdr_card_data);
+ icco.length = 0x72;
+ icco.status = Common::swap16(icco.extlen);
+
+ icco.pktlen += icco.extlen;
+
+ memcpy(icco.extdata, s_cdr_card_data, sizeof(s_cdr_card_data));
+
+ break;
+ default:
+ if (!validate_data_in_out(14, 0, "SerialA (DECK READER)"))
+ break;
+ WARN_LOG_FMT(SERIALINTERFACE_CARD,
+ "GC-AM: Command 0x31 (IC-Card) {:02x} {:02x} {:02x} {:02x} {:02x} "
+ "{:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x}",
+ data_in[2], data_in[3], data_in[4], data_in[5], data_in[6],
+ data_in[7], data_in[8], data_in[9], data_in[10], data_in[11],
+ data_in[12], data_in[13]);
+ break;
+ }
+ break;
+ }
+
+ if (!validate_data_in_out(0, icco.pktlen + 2, "SerialA (IC-CARD)"))
+ break;
+ ICCardSendReply(&icco, data_out.data(), &data_offset);
+
+ if (!validate_data_in_out(2, 0, "SerialA (IC-CARD)"))
+ break;
+ data_in += length;
+ break;
+ }
+ }
+
+ u32 command_offset = 0;
+ while (command_offset < length)
+ {
+ // All commands are OR'd with 0x80
+ // Last byte is checksum which we don't care about
+ if (!validate_data_in_out(command_offset + 4, 0, "SerialA"))
+ break;
+ const u32 serial_command = Common::swap32(data_in + command_offset) ^ 0x80000000;
+
+ if (AMMediaboard::GetGameType() == FZeroAX ||
+ AMMediaboard::GetGameType() == FZeroAXMonster)
+ {
+ INFO_LOG_FMT(SERIALINTERFACE_AMBB,
+ "GC-AM: Command 0x31 (MOTOR) Length:{:02x} Command:{:04x}({:02x})",
+ length, (serial_command >> 8) & 0xFFFF, serial_command >> 24);
+ }
+ else
+ {
+ INFO_LOG_FMT(SERIALINTERFACE_AMBB, "GC-AM: Command 0x31 (SERIAL) Command:{:06x}",
+ serial_command);
+
+ if (serial_command == 0x801000)
+ {
+ if (!validate_data_in_out(0, 4, "SerialA"))
+ break;
+ data_out[data_offset++] = 0x31;
+ data_out[data_offset++] = 0x02;
+ data_out[data_offset++] = 0xFF;
+ data_out[data_offset++] = 0x01;
+ }
+ }
+
+ command_offset += sizeof(u32);
+
+ if (AMMediaboard::GetGameType() == FZeroAX ||
+ AMMediaboard::GetGameType() == FZeroAXMonster)
+ {
+ if (command_offset + 5 >= std::size(m_motor_reply))
+ {
+ ERROR_LOG_FMT(
+ SERIALINTERFACE_AMBB,
+ "GC-AM: Command 0x31 (MOTOR) overflow: offset={} >= motor_reply_size={}",
+ command_offset + 5, std::size(m_motor_reply));
+ data_in = data_in_end;
+ break;
+ }
+
+ // Status
+ m_motor_reply[command_offset + 2] = 0;
+ m_motor_reply[command_offset + 3] = 0;
+
+ // Error
+ m_motor_reply[command_offset + 4] = 0;
+
+ switch (serial_command >> 24)
+ {
+ case 0:
+ case 1: // Set Maximum?
+ case 2:
+ break;
+
+ // 0x00-0x40: left
+ // 0x40-0x80: right
+ case 4: // Move Steering Wheel
+ // Left
+ if (serial_command & 0x010000)
+ {
+ m_motor_force_y = -((s16)serial_command & 0xFF00);
+ }
+ else // Right
+ {
+ m_motor_force_y = (serial_command - 0x4000) & 0xFF00;
+ }
+
+ m_motor_force_y *= 2;
+
+ // FFB
+ if (m_motor_init == 2)
+ {
+ if (serial_interface.GetDeviceType(1) == SerialInterface::SIDEVICE_GC_STEERING)
+ {
+ const GCPadStatus pad_status = Pad::GetStatus(1);
+ if (pad_status.isConnected)
+ {
+ const ControlState mapped_strength = (double)(m_motor_force_y >> 8) / 127.f;
+
+ Pad::Rumble(1, mapped_strength);
+ INFO_LOG_FMT(SERIALINTERFACE_AMBB,
+ "GC-AM: Command 0x31 (MOTOR) mapped_strength:{}",
+ mapped_strength);
+ }
+ }
+ }
+ break;
+ case 6: // nice
+ case 9:
+ default:
+ break;
+ // Switch back to normal controls
+ case 7:
+ m_motor_init = 2;
+ break;
+ // Reset
+ case 0x7F:
+ m_motor_init = 1;
+ memset(m_motor_reply, 0, sizeof(m_motor_reply));
+ break;
+ }
+
+ // Checksum
+ m_motor_reply[command_offset + 5] = m_motor_reply[command_offset + 2] ^
+ m_motor_reply[command_offset + 3] ^
+ m_motor_reply[command_offset + 4];
+ }
+ }
+
+ if (length == 0)
+ {
+ if (!validate_data_in_out(length, 2, "SerialA"))
+ break;
+ data_out[data_offset++] = gcam_command;
+ data_out[data_offset++] = 0x00;
+ }
+ else
+ {
+ if (m_motor_init)
+ {
+ // Motor
+ m_motor_reply[0] = gcam_command;
+ m_motor_reply[1] = length; // Same out as in size
+
+ const u32 reply_size = m_motor_reply[1] + 2;
+ if (!validate_data_in_out(length, reply_size, "SerialA"))
+ break;
+
+ if (reply_size > sizeof(m_motor_reply))
+ {
+ ERROR_LOG_FMT(SERIALINTERFACE_AMBB,
+ "GC-AM: Command SerialA, reply_size={} too big for m_motor_reply",
+ reply_size);
+ data_in = data_in_end;
+ break;
+ }
+ memcpy(data_out.data() + data_offset, m_motor_reply, reply_size);
+ data_offset += reply_size;
+ }
+ }
+
+ if (!validate_data_in_out(length, 0, "SerialA"))
+ {
+ break;
+ }
+ data_in += length;
+ break;
+ }
+ case GCAMCommand::SerialB:
+ {
+ DEBUG_LOG_FMT(SERIALINTERFACE_AMBB, "GC-AM: Command 32 (CARD-Interface)");
+
+ if (!validate_data_in_out(1, 0, "SerialB"))
+ break;
+ const u32 in_length = *data_in++;
+
+ static constexpr u32 max_packet_size = 0x2f;
+
+ // Also accounting for the 2-byte header.
+ if (!validate_data_in_out(in_length, max_packet_size + 2, "SerialB"))
+ break;
+
+ if (m_mag_card_reader)
+ {
+ // Append the data to our buffer.
+ const auto prev_size = m_mag_card_in_buffer.size();
+ m_mag_card_in_buffer.resize(prev_size + in_length);
+ std::ranges::copy(std::span{data_in, in_length},
+ m_mag_card_in_buffer.data() + prev_size);
+
+ // Send and receive data with the magnetic card reader.
+ m_mag_card_reader->Process(&m_mag_card_in_buffer, &m_mag_card_out_buffer);
+ }
+
+ data_in += in_length;
+ const auto out_length = std::min(u32(m_mag_card_out_buffer.size()), max_packet_size);
+
+ // Write the 2-byte header.
+ data_out[data_offset++] = gcam_command;
+ data_out[data_offset++] = u8(out_length);
+
+ // Write the data.
+ std::copy_n(m_mag_card_out_buffer.data(), out_length, data_out.data() + data_offset);
+ data_offset += out_length;
+
+ // Remove the data from our buffer.
+ m_mag_card_out_buffer.erase(m_mag_card_out_buffer.begin(),
+ m_mag_card_out_buffer.begin() + s32(out_length));
+ break;
+ }
+ case GCAMCommand::JVSIOA:
+ case GCAMCommand::JVSIOB:
+ {
+ if (!validate_data_in_out(4, 0, "JVSIO"))
+ break;
+
+ JVSIOMessage message;
+
+ const u8* const frame = &data_in[0];
+ const u8 nr_bytes = frame[3]; // Byte after E0 xx
+ u32 frame_len = nr_bytes + 3; // Header(2) + length byte + payload + checksum
+
+ u8 jvs_buf[0x80];
+
+ frame_len = std::min<u32>(frame_len, sizeof(jvs_buf));
+
+ if (!validate_data_in_out(frame_len, 0, "JVSIO"))
+ break;
+ DEBUG_LOG_FMT(SERIALINTERFACE_JVSIO, "GC-AM: Command {:02x} (JVS IO), hexdump:\n{}",
+ gcam_command, HexDump(data_in, frame_len));
+
+ memcpy(jvs_buf, frame, frame_len);
+
+ // Extract node and payload pointers
+ u8 node = jvs_buf[2];
+ u8* jvs_io = jvs_buf + 4; // First payload byte
+ u8* const jvs_end = jvs_buf + frame_len; // One byte before checksum
+ u8* const jvs_begin = jvs_io;
+
+ message.Start(0);
+ message.AddData(1);
+
+ // Helper to check that iterating over jvs_io n times is safe,
+ // i.e. *jvs_io++ at most lead to jvs_end
+ auto validate_jvs_io = [&](u32 n, std::string_view command) -> bool {
+ if (jvs_io + n > jvs_end)
+ ERROR_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: overflow in {}", command);
+ else
+ return true;
+ ERROR_LOG_FMT(SERIALINTERFACE_JVSIO,
+ "Overflow details:\n"
+ " - jvs_io(begin={}, current={}, end={}, n={})\n"
+ " - delay={}, node={}\n"
+ " - frame(begin={}, len={})",
+ fmt::ptr(jvs_begin), fmt::ptr(jvs_io), fmt::ptr(jvs_end), n, m_delay,
+ node, fmt::ptr(frame), frame_len);
+ jvs_io = jvs_end;
+ return false;
+ };
+
+ // Now iterate over the payload
+ while (jvs_io < jvs_end)
+ {
+ const u8 jvsio_command = *jvs_io++;
+ DEBUG_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO:node={}, command={:02x}", node,
+ jvsio_command);
+
+ switch (JVSIOCommand(jvsio_command))
+ {
+ case JVSIOCommand::IOID:
+ message.AddData(StatusOkay);
+ switch (AMMediaboard::GetGameType())
+ {
+ case FZeroAX:
+ // Specific version that enables DX mode on AX machines, all this does is enable the
+ // motion of a chair
+ message.AddData("SEGA ENTERPRISES,LTD.;837-13844-01 I/O CNTL BD2 ;");
+ break;
+ case FZeroAXMonster:
+ case MarioKartGP:
+ case MarioKartGP2:
+ default:
+ message.AddData("namco ltd.;FCA-1;Ver1.01;JPN,Multipurpose + Rotary Encoder");
+ break;
+ case VirtuaStriker3:
+ case VirtuaStriker4:
+ case VirtuaStriker4_2006:
+ message.AddData("SEGA ENTERPRISES,LTD.;I/O BD JVS;837-13551;Ver1.00");
+ break;
+ }
+ NOTICE_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Command 0x10, BoardID");
+ message.AddData((u32)0);
+ break;
+ case JVSIOCommand::CommandRevision:
+ message.AddData(StatusOkay);
+ message.AddData(0x11);
+ NOTICE_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Command 0x11, CommandRevision");
+ break;
+ case JVSIOCommand::JVRevision:
+ message.AddData(StatusOkay);
+ message.AddData(0x20);
+ NOTICE_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Command 0x12, JVRevision");
+ break;
+ case JVSIOCommand::CommunicationVersion:
+ message.AddData(StatusOkay);
+ message.AddData(0x10);
+ NOTICE_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Command 0x13, CommunicationVersion");
+ break;
+
+ // Slave features:
+ //
+ // Inputs:
+ // 0x01: Switch input: players, buttons
+ // 0x02: Coin input: slots
+ // 0x03: Analog input: channels, bits
+ // 0x04: Rotary input: channels
+ // 0x05: Keycode input: 0,0,0 ?
+ // 0x06: Screen position input: X bits, Y bits, channels
+ //
+ // Outputs:
+ // 0x10: Card system: slots
+ // 0x11: Medal hopper: channels
+ // 0x12: GPO-out: slots
+ // 0x13: Analog output: channels
+ // 0x14: Character output: width, height, type
+ // 0x15: Backup
+ case JVSIOCommand::CheckFunctionality:
+ message.AddData(StatusOkay);
+ switch (AMMediaboard::GetGameType())
+ {
+ case FZeroAX:
+ case FZeroAXMonster:
+ // 2 Player (12bit) (p2=paddles), 1 Coin slot, 6 Analog-in
+ // message.AddData((void *)"\x01\x02\x0C\x00", 4);
+ // message.AddData((void *)"\x02\x01\x00\x00", 4);
+ // message.AddData((void *)"\x03\x06\x00\x00", 4);
+ // message.AddData((void *)"\x00\x00\x00\x00", 4);
+ //
+ // DX Version: 2 Player (22bit) (p2=paddles), 2 Coin slot, 8 Analog-in,
+ // 22 Driver-out
+ message.AddData("\x01\x02\x12\x00", 4);
+ message.AddData("\x02\x02\x00\x00", 4);
+ message.AddData("\x03\x08\x0A\x00", 4);
+ message.AddData("\x12\x16\x00\x00", 4);
+ message.AddData("\x00\x00\x00\x00", 4);
+ break;
+ case VirtuaStriker3:
+ // 2 Player (13bit), 2 Coin slot, 4 Analog-in, 1 CARD, 8 Driver-out
+ message.AddData("\x01\x02\x0D\x00", 4);
+ message.AddData("\x02\x02\x00\x00", 4);
+ message.AddData("\x10\x01\x00\x00", 4);
+ message.AddData("\x12\x08\x00\x00", 4);
+ message.AddData("\x00\x00\x00\x00", 4);
+ break;
+ case GekitouProYakyuu:
+ // 2 Player (13bit), 2 Coin slot, 4 Analog-in, 1 CARD, 8 Driver-out
+ message.AddData("\x01\x02\x0D\x00", 4);
+ message.AddData("\x02\x02\x00\x00", 4);
+ message.AddData("\x03\x04\x00\x00", 4);
+ message.AddData("\x10\x01\x00\x00", 4);
+ message.AddData("\x12\x08\x00\x00", 4);
+ message.AddData("\x00\x00\x00\x00", 4);
+ break;
+ case VirtuaStriker4:
+ case VirtuaStriker4_2006:
+ // 2 Player (13bit), 1 Coin slot, 4 Analog-in, 1 CARD
+ message.AddData("\x01\x02\x0D\x00", 4);
+ message.AddData("\x02\x01\x00\x00", 4);
+ message.AddData("\x03\x04\x00\x00", 4);
+ message.AddData("\x10\x01\x00\x00", 4);
+ message.AddData("\x00\x00\x00\x00", 4);
+ break;
+ case KeyOfAvalon:
+ // 1 Player (15bit), 1 Coin slot, 3 Analog-in, Touch, 1 CARD, 1 Driver-out
+ // (Unconfirmed)
+ message.AddData("\x01\x01\x0F\x00", 4);
+ message.AddData("\x02\x01\x00\x00", 4);
+ message.AddData("\x03\x03\x00\x00", 4);
+ message.AddData("\x06\x10\x10\x01", 4);
+ message.AddData("\x10\x01\x00\x00", 4);
+ message.AddData("\x12\x01\x00\x00", 4);
+ message.AddData("\x00\x00\x00\x00", 4);
+ break;
+ case MarioKartGP:
+ case MarioKartGP2:
+ default:
+ // 1 Player (15bit), 1 Coin slot, 3 Analog-in, 1 CARD, 1 Driver-out
+ message.AddData("\x01\x01\x0F\x00", 4);
+ message.AddData("\x02\x01\x00\x00", 4);
+ message.AddData("\x03\x03\x00\x00", 4);
+ message.AddData("\x10\x01\x00\x00", 4);
+ message.AddData("\x12\x01\x00\x00", 4);
+ message.AddData("\x00\x00\x00\x00", 4);
+ break;
+ }
+ NOTICE_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Command 0x14, CheckFunctionality");
+ break;
+ case JVSIOCommand::MainID:
+ {
+ const u8* const main_id = jvs_io;
+ while (jvs_io < jvs_end && *jvs_io++)
+ {
+ }
+ if (main_id < jvs_io)
+ {
+ DEBUG_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Command MainId:\n{}",
+ HexDump(main_id, jvs_io - main_id));
+ }
+ message.AddData(StatusOkay);
+ break;
+ }
+ case JVSIOCommand::SwitchesInput:
+ {
+ if (!validate_jvs_io(2, "SwitchesInput"))
+ break;
+ const u32 player_count = *jvs_io++;
+ const u32 player_byte_count = *jvs_io++;
+
+ DEBUG_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Command 0x20, SwitchInputs: {} {}",
+ player_count, player_byte_count);
+
+ message.AddData(StatusOkay);
+
+ GCPadStatus pad_status = Pad::GetStatus(0);
+
+ // Test button
+ if (pad_status.switches & SWITCH_TEST)
+ {
+ // Trying to access the test menu without SegaBoot present will cause a crash
+ if (AMMediaboard::GetTestMenu())
+ {
+ message.AddData(0x80);
+ }
+ else
+ {
+ PanicAlertFmt("Test menu is disabled due to missing SegaBoot");
+ }
+ }
+ else
+ {
+ message.AddData((u32)0x00);
+ }
+
+ for (u32 i = 0; i < player_count; ++i)
+ {
+ std::array<u8, 3> player_data{};
+
+ // Service button
+ if (pad_status.switches & SWITCH_SERVICE)
+ player_data[0] |= 0x40;
+
+ switch (AMMediaboard::GetGameType())
+ {
+ // Controller configuration for F-Zero AX (DX)
+ case FZeroAX:
+ if (i == 0)
+ {
+ if (m_fzdx_seatbelt)
+ {
+ player_data[0] |= 0x01;
+ }
+
+ // Start
+ if (pad_status.button & PAD_BUTTON_START)
+ player_data[0] |= 0x80;
+ // Boost
+ if (pad_status.button & PAD_BUTTON_A)
+ player_data[0] |= 0x02;
+ // View Change 1
+ if (pad_status.button & PAD_BUTTON_RIGHT)
+ player_data[0] |= 0x20;
+ // View Change 2
+ if (pad_status.button & PAD_BUTTON_LEFT)
+ player_data[0] |= 0x10;
+ // View Change 3
+ if (pad_status.button & PAD_BUTTON_UP)
+ player_data[0] |= 0x08;
+ // View Change 4
+ if (pad_status.button & PAD_BUTTON_DOWN)
+ player_data[0] |= 0x04;
+ player_data[1] = m_rx_reply & 0xF0;
+ }
+ else if (i == 1)
+ {
+ // Paddle left
+ if (pad_status.button & PAD_BUTTON_X)
+ player_data[0] |= 0x20;
+ // Paddle right
+ if (pad_status.button & PAD_BUTTON_Y)
+ player_data[0] |= 0x10;
+
+ if (m_fzdx_motion_stop)
+ {
+ player_data[0] |= 2;
+ }
+ if (m_fzdx_sensor_right)
+ {
+ player_data[0] |= 4;
+ }
+ if (m_fzdx_sensor_left)
+ {
+ player_data[0] |= 8;
+ }
+
+ player_data[1] = m_rx_reply << 4;
+ }
+ break;
+ // Controller configuration for F-Zero AX MonsterRide
+ case FZeroAXMonster:
+ if (i == 0)
+ {
+ if (m_fzcc_sensor)
+ {
+ player_data[0] |= 0x01;
+ }
+
+ // Start
+ if (pad_status.button & PAD_BUTTON_START)
+ player_data[0] |= 0x80;
+ // Boost
+ if (pad_status.button & PAD_BUTTON_A)
+ player_data[0] |= 0x02;
+ // View Change 1
+ if (pad_status.button & PAD_BUTTON_RIGHT)
+ player_data[0] |= 0x20;
+ // View Change 2
+ if (pad_status.button & PAD_BUTTON_LEFT)
+ player_data[0] |= 0x10;
+ // View Change 3
+ if (pad_status.button & PAD_BUTTON_UP)
+ player_data[0] |= 0x08;
+ // View Change 4
+ if (pad_status.button & PAD_BUTTON_DOWN)
+ player_data[0] |= 0x04;
+
+ player_data[1] = m_rx_reply & 0xF0;
+ }
+ else if (i == 1)
+ {
+ // Paddle left
+ if (pad_status.button & PAD_BUTTON_X)
+ player_data[0] |= 0x20;
+ // Paddle right
+ if (pad_status.button & PAD_BUTTON_Y)
+ player_data[0] |= 0x10;
+
+ if (m_fzcc_seatbelt)
+ {
+ player_data[0] |= 2;
+ }
+ if (m_fzcc_service)
+ {
+ player_data[0] |= 4;
+ }
+ if (m_fzcc_emergency)
+ {
+ player_data[0] |= 8;
+ }
+ }
+ break;
+ // Controller configuration for Virtua Striker 3 games
+ case VirtuaStriker3:
+ pad_status = Pad::GetStatus(i);
+ // Start
+ if (pad_status.button & PAD_BUTTON_START)
+ player_data[0] |= 0x80;
+ // Shoot
+ if (pad_status.button & PAD_BUTTON_B)
+ player_data[0] |= 0x01;
+ // Short Pass
+ if (pad_status.button & PAD_BUTTON_A)
+ player_data[1] |= 0x80;
+ // Long Pass
+ if (pad_status.button & PAD_BUTTON_X)
+ player_data[0] |= 0x02;
+ // Left
+ if (pad_status.button & PAD_BUTTON_LEFT)
+ player_data[0] |= 0x08;
+ // Up
+ if (pad_status.button & PAD_BUTTON_UP)
+ player_data[0] |= 0x20;
+ // Right
+ if (pad_status.button & PAD_BUTTON_RIGHT)
+ player_data[0] |= 0x04;
+ // Down
+ if (pad_status.button & PAD_BUTTON_DOWN)
+ player_data[0] |= 0x10;
+ break;
+ // Controller configuration for Virtua Striker 4 games
+ case VirtuaStriker4:
+ case VirtuaStriker4_2006:
+ {
+ pad_status = Pad::GetStatus(i);
+ // Start
+ if (pad_status.button & PAD_BUTTON_START)
+ player_data[0] |= 0x80;
+ // Long Pass
+ if (pad_status.button & PAD_BUTTON_X)
+ player_data[0] |= 0x01;
+ // Short Pass
+ if (pad_status.button & PAD_BUTTON_A)
+ player_data[0] |= 0x02;
+ // Shoot
+ if (pad_status.button & PAD_BUTTON_B)
+ player_data[1] |= 0x80;
+ // Dash
+ if (pad_status.button & PAD_BUTTON_Y)
+ player_data[1] |= 0x40;
+ // Tactics (U)
+ if (pad_status.button & PAD_BUTTON_LEFT)
+ player_data[0] |= 0x20;
+ // Tactics (M)
+ if (pad_status.button & PAD_BUTTON_UP)
+ player_data[0] |= 0x08;
+ // Tactics (D)
+ if (pad_status.button & PAD_BUTTON_RIGHT)
+ player_data[0] |= 0x04;
+
+ if (i == 0)
+ {
+ player_data[0] |= 0x10; // IC-Card Switch ON
+
+ // IC-Card Lock
+ if (pad_status.button & PAD_BUTTON_DOWN)
+ player_data[1] |= 0x20;
+ }
+ }
+ break;
+ // Controller configuration for Gekitou Pro Yakyuu
+ case GekitouProYakyuu:
+ pad_status = Pad::GetStatus(i);
+ // Start
+ if (pad_status.button & PAD_BUTTON_START)
+ player_data[0] |= 0x80;
+ // A
+ if (pad_status.button & PAD_BUTTON_B)
+ player_data[0] |= 0x01;
+ // B
+ if (pad_status.button & PAD_BUTTON_A)
+ player_data[0] |= 0x02;
+ // Gekitou
+ if (pad_status.button & PAD_TRIGGER_L)
+ player_data[1] |= 0x80;
+ // Left
+ if (pad_status.button & PAD_BUTTON_LEFT)
+ player_data[0] |= 0x08;
+ // Up
+ if (pad_status.button & PAD_BUTTON_UP)
+ player_data[0] |= 0x20;
+ // Right
+ if (pad_status.button & PAD_BUTTON_RIGHT)
+ player_data[0] |= 0x04;
+ // Down
+ if (pad_status.button & PAD_BUTTON_DOWN)
+ player_data[0] |= 0x10;
+ break;
+ // Controller configuration for Mario Kart and other games
+ default:
+ case MarioKartGP:
+ case MarioKartGP2:
+ {
+ // Start
+ if (pad_status.button & PAD_BUTTON_START)
+ player_data[0] |= 0x80;
+ // Item button
+ if (pad_status.button & PAD_BUTTON_A)
+ player_data[1] |= 0x20;
+ // VS-Cancel button
+ if (pad_status.button & PAD_BUTTON_B)
+ player_data[1] |= 0x02;
+ }
+ break;
+ case KeyOfAvalon:
+ {
+ // Debug On
+ if (pad_status.button & PAD_BUTTON_START)
+ player_data[0] |= 0x80;
+ // Switch 1
+ if (pad_status.button & PAD_BUTTON_A)
+ player_data[0] |= 0x04;
+ // Switch 2
+ if (pad_status.button & PAD_BUTTON_B)
+ player_data[0] |= 0x08;
+ // Toggle inserted card
+ if (pad_status.button & PAD_TRIGGER_L)
+ {
+ m_ic_card_status ^= ICCARDStatus::NoCard;
+ }
+ }
+ break;
+ }
+
+ if (player_byte_count > player_data.size())
+ {
+ WARN_LOG_FMT(SERIALINTERFACE_JVSIO,
+ "JVS-IO: Command 0x20, SwitchInputs: invalid player_byte_count={}",
+ player_byte_count);
+ }
+ const u32 data_size = std::min(player_byte_count, u32(player_data.size()));
+ for (u32 j = 0; j < data_size; ++j)
+ message.AddData(player_data[j]);
+ }
+ break;
+ }
+ case JVSIOCommand::CoinInput:
+ {
+ if (!validate_jvs_io(1, "CoinInput"))
+ break;
+ const u32 slots = *jvs_io++;
+ message.AddData(StatusOkay);
+ static_assert(std::tuple_size<decltype(m_coin)>{} == 2 &&
+ std::tuple_size<decltype(m_coin_pressed)>{} == 2);
+ if (slots > 2)
+ {
+ WARN_LOG_FMT(SERIALINTERFACE_JVSIO,
+ "JVS-IO: Command 0x21, CoinInput: invalid slots {}", slots);
+ }
+ const u32 max_slots = std::min(slots, 2u);
+ for (u32 i = 0; i < max_slots; i++)
+ {
+ GCPadStatus pad_status = Pad::GetStatus(i);
+ if ((pad_status.switches & SWITCH_COIN) && !m_coin_pressed[i])
+ {
+ m_coin[i]++;
+ }
+ m_coin_pressed[i] = pad_status.switches & SWITCH_COIN;
+ message.AddData((m_coin[i] >> 8) & 0x3f);
+ message.AddData(m_coin[i] & 0xff);
+ }
+ DEBUG_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Command 0x21, CoinInput: {}", slots);
+ break;
+ }
+ case JVSIOCommand::AnalogInput:
+ {
+ if (!validate_jvs_io(1, "AnalogInput"))
+ break;
+ message.AddData(StatusOkay);
+
+ const u32 analogs = *jvs_io++;
+ GCPadStatus pad_status = Pad::GetStatus(0);
+
+ DEBUG_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Command 0x22, AnalogInput: {}",
+ analogs);
+
+ switch (AMMediaboard::GetGameType())
+ {
+ case FZeroAX:
+ case FZeroAXMonster:
+ // Steering
+ if (m_motor_init == 1)
+ {
+ if (m_motor_force_y > 0)
+ {
+ message.AddData(0x80 - (m_motor_force_y >> 8));
+ }
+ else
+ {
+ message.AddData((m_motor_force_y >> 8));
+ }
+ message.AddData((u8)0);
+
+ message.AddData(pad_status.stickY);
+ message.AddData((u8)0);
+ }
+ else
+ {
+ // The center for the Y axis is expected to be 78h this adjusts that
+ message.AddData(pad_status.stickX - 12);
+ message.AddData((u8)0);
+
+ message.AddData(pad_status.stickY);
+ message.AddData((u8)0);
+ }
+
+ // Unused
+ message.AddData((u8)0);
+ message.AddData((u8)0);
+ message.AddData((u8)0);
+ message.AddData((u8)0);
+
+ // Gas
+ message.AddData(pad_status.triggerRight);
+ message.AddData((u8)0);
+
+ // Brake
+ message.AddData(pad_status.triggerLeft);
+ message.AddData((u8)0);
+
+ message.AddData((u8)0x80); // Motion Stop
+ message.AddData((u8)0);
+
+ message.AddData((u8)0);
+ message.AddData((u8)0);
+
+ break;
+ case VirtuaStriker4:
+ case VirtuaStriker4_2006:
+ {
+ message.AddData(-pad_status.stickY);
+ message.AddData((u8)0);
+ message.AddData(pad_status.stickX);
+ message.AddData((u8)0);
+
+ pad_status = Pad::GetStatus(1);
+
+ message.AddData(-pad_status.stickY);
+ message.AddData((u8)0);
+ message.AddData(pad_status.stickX);
+ message.AddData((u8)0);
+ }
+ break;
+ default:
+ case MarioKartGP:
+ case MarioKartGP2:
+ // Steering
+ message.AddData(pad_status.stickX);
+ message.AddData((u8)0);
+
+ // Gas
+ message.AddData(pad_status.triggerRight);
+ message.AddData((u8)0);
+
+ // Brake
+ message.AddData(pad_status.triggerLeft);
+ message.AddData((u8)0);
+ break;
+ }
+ break;
+ }
+ case JVSIOCommand::PositionInput:
+ {
+ if (!validate_jvs_io(1, "PositionInput"))
+ break;
+ const u32 channel = *jvs_io++;
+
+ const GCPadStatus pad_status = Pad::GetStatus(0);
+
+ if (pad_status.button & PAD_TRIGGER_R)
+ {
+ // Tap at center of screen (~320,240)
+ message.AddData("\x01\x00\x8C\x01\x95",
+ 5); // X=320 (0x0140), Y=240 (0x00F0)
+ }
+ else
+ {
+ message.AddData("\x01\xFF\xFF\xFF\xFF", 5);
+ }
+
+ DEBUG_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Command 0x25, PositionInput:{}",
+ channel);
+ break;
+ }
+ case JVSIOCommand::CoinSubOutput:
+ {
+ if (!validate_jvs_io(3, "CoinSubOutput"))
+ break;
+ const u32 slot = *jvs_io++;
+ const u8 coinh = *jvs_io++;
+ const u8 coinl = *jvs_io++;
+
+ if (slot < m_coin.size())
+ {
+ m_coin[slot] -= (coinh << 8) | coinl;
+ }
+ else
+ {
+ WARN_LOG_FMT(SERIALINTERFACE_JVSIO,
+ "JVS-IO: Command 0x30, CoinSubOutput: invalid slot {}", slot);
+ }
+
+ message.AddData(StatusOkay);
+ DEBUG_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Command 0x30, CoinSubOutput: {}", slot);
+ break;
+ }
+ case JVSIOCommand::GeneralDriverOutput:
+ {
+ if (!validate_jvs_io(1, "GeneralDriverOutput"))
+ break;
+ const u32 bytes = *jvs_io++;
+
+ if (bytes)
+ {
+ message.AddData(StatusOkay);
+
+ // The lamps are controlled via this
+ if (AMMediaboard::GetGameType() == MarioKartGP)
+ {
+ if (!validate_jvs_io(1, "GeneralDriverOutput (MarioKartGP)"))
+ break;
+ const u32 status = *jvs_io++;
+ if (status & 4)
+ {
+ DEBUG_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Command 32, Item Button ON");
+ }
+ else
+ {
+ DEBUG_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Command 32, Item Button OFF");
+ }
+ if (status & 8)
+ {
+ DEBUG_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Command 32, Cancel Button ON");
+ }
+ else
+ {
+ DEBUG_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Command 32, Cancel Button OFF");
+ }
+ break;
+ }
+
+ if (!validate_jvs_io(bytes, "GeneralDriverOutput"))
+ break;
+
+ INFO_LOG_FMT(SERIALINTERFACE_JVSIO,
+ "JVS-IO: Command 0x32, GPO: delay=0x{:02x}, rx_reply=0x{:02x},"
+ " bytes={}, buffer:\n{}",
+ m_delay, m_rx_reply, bytes, HexDump(jvs_io, bytes));
+
+ if (bytes < 3)
+ {
+ jvs_io += bytes;
+ break;
+ }
+
+ // Handling of the motion seat used in F-Zero AXs DX version
+ const u16 seat_state = Common::swap16(jvs_io + 1) >> 2;
+ jvs_io += bytes;
+
+ switch (seat_state)
+ {
+ case 0x70:
+ m_delay++;
+ if ((m_delay % 10) == 0)
+ {
+ m_rx_reply = 0xFB;
+ }
+ break;
+ case 0xF0:
+ m_rx_reply = 0xF0;
+ break;
+ default:
+ case 0xA0:
+ case 0x60:
+ break;
+ }
+ }
+ break;
+ }
+ case JVSIOCommand::CoinAddOutput:
+ {
+ if (!validate_jvs_io(3, "CoinAddOutput"))
+ break;
+ const u32 slot = *jvs_io++;
+ const u8 coinh = *jvs_io++;
+ const u8 coinl = *jvs_io++;
+
+ m_coin[slot] += (coinh << 8) | coinl;
+
+ message.AddData(StatusOkay);
+ DEBUG_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Command 0x35, CoinAddOutput: {}", slot);
+ break;
+ }
+ case JVSIOCommand::NAMCOCommand:
+ {
+ if (!validate_jvs_io(1, "NAMCOCommand"))
+ break;
+ const u32 namco_command = *jvs_io++;
+
+ if (namco_command == 0x18)
+ {
+ if (!validate_jvs_io(4, "NAMCOCommand(0x18) / ID check"))
+ break;
+ // ID check
+ jvs_io += 4;
+ message.AddData(StatusOkay);
+ message.AddData(0xff);
+ }
+ else
+ {
+ message.AddData(StatusOkay);
+ ERROR_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Unknown:{:02x}", namco_command);
+ }
+ break;
+ }
+ case JVSIOCommand::Reset:
+ if (!validate_jvs_io(1, "Reset"))
+ break;
+ if (*jvs_io++ == 0xD9)
+ {
+ NOTICE_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Command 0xF0, Reset");
+ m_delay = 0;
+ m_wheel_init = 0;
+ m_ic_card_state = 0x20;
+ }
+ message.AddData(StatusOkay);
+
+ m_dip_switch_1 |= 1;
+ break;
+ case JVSIOCommand::SetAddress:
+ if (!validate_jvs_io(1, "SetAddress"))
+ break;
+ node = *jvs_io++;
+ NOTICE_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Command 0xF1, SetAddress: node={}",
+ node);
+ message.AddData(node == 1);
+ m_dip_switch_1 &= ~1u;
+ break;
+ default:
+ ERROR_LOG_FMT(SERIALINTERFACE_JVSIO, "JVS-IO: Unhandled: node={}, command={:02x}",
+ node, jvsio_command);
+ break;
+ }
+ }
+
+ message.End();
+
+ if (!validate_data_in_out(0, 2, "JVSIO"))
+ break;
+ data_out[data_offset++] = gcam_command;
+
+ const u8* buf = message.m_message.data();
+ const u32 len = message.m_pointer;
+ data_out[data_offset++] = len;
+ const u32 in_size = frame[0] + 1;
+
+ if (!validate_data_in_out(in_size, len, "JVSIO"))
+ break;
+ for (u32 i = 0; i < len; ++i)
+ data_out[data_offset++] = buf[i];
+
+ data_in += in_size;
+ break;
+ }
+ case GCAMCommand::Unknown_60:
+ {
+ if (!validate_data_in_out(3, 0, "Unknown_60"))
+ break;
+
+ NOTICE_LOG_FMT(SERIALINTERFACE_AMBB, "GC-AM: Command 0x60, {:02x} {:02x} {:02x}",
+ data_in[0], data_in[1], data_in[2]);
+
+ const u32 in_size = data_in[0] + 1;
+ if (!validate_data_in_out(in_size, 0, "Unknown_60"))
+ break;
+ data_in += in_size;
+ }
+ break;
+ default:
+ if (!validate_data_in_out(5, 0, fmt::format("Unknown_{}", gcam_command)))
+ break;
+ ERROR_LOG_FMT(SERIALINTERFACE_AMBB,
+ "GC-AM: Command {:02x} (unknown) {:02x} {:02x} {:02x} {:02x} {:02x}",
+ gcam_command, data_in[0], data_in[1], data_in[2], data_in[3], data_in[4]);
+ break;
+ }
+ }
+
+ memset(buffer, 0, buffer_length);
+
+ data_in = buffer;
+ data_out[1] = data_offset - 2;
+ checksum = 0;
+
+ if (buffer_length >= 0x80)
+ {
+ for (int i = 0; i < 0x7F; ++i)
+ {
+ checksum += data_in[i] = data_out[i];
+ }
+ data_in[0x7f] = ~checksum;
+ DEBUG_LOG_FMT(SERIALINTERFACE_AMBB, "Command send back: {}",
+ HexDump(data_out.data(), 0x7F));
+ }
+ else
+ {
+ ERROR_LOG_FMT(SERIALINTERFACE_AMBB, "GC-AM: overflow in GCAM_Command's checksum");
+ }
+
+ SwapBuffers(buffer, &buffer_length);
+
+ buffer_position = buffer_length;
+ break;
+ }
+ default:
+ {
+ ERROR_LOG_FMT(SERIALINTERFACE, "Unknown SI command (0x{:08x})", (u32)bb_command);
+ PanicAlertFmt("SI: Unknown command");
+ buffer_position = buffer_length;
+ }
+ break;
+ }
+ }
+
+ return buffer_position;
+}
+
+u32 CSIDevice_AMBaseboard::MapPadStatus(const GCPadStatus& pad_status)
+{
+ // Thankfully changing mode does not change the high bits ;)
+ u32 hi = 0;
+ hi = pad_status.stickY;
+ hi |= pad_status.stickX << 8;
+ hi |= (pad_status.button | PAD_USE_ORIGIN) << 16;
+ return hi;
+}
+
+CSIDevice_AMBaseboard::EButtonCombo
+CSIDevice_AMBaseboard::HandleButtonCombos(const GCPadStatus& pad_status)
+{
+ // Keep track of the special button combos (embedded in controller hardware... :( )
+ EButtonCombo temp_combo = COMBO_NONE;
+ if ((pad_status.button & 0xff00) == (PAD_BUTTON_Y | PAD_BUTTON_X | PAD_BUTTON_START))
+ temp_combo = COMBO_ORIGIN;
+ else if ((pad_status.button & 0xff00) == (PAD_BUTTON_B | PAD_BUTTON_X | PAD_BUTTON_START))
+ temp_combo = COMBO_RESET;
+
+ if (temp_combo != m_last_button_combo)
+ {
+ m_last_button_combo = temp_combo;
+ if (m_last_button_combo != COMBO_NONE)
+ m_timer_button_combo_start = m_system.GetCoreTiming().GetTicks();
+ }
+
+ if (m_last_button_combo != COMBO_NONE)
+ {
+ const u64 current_time = m_system.GetCoreTiming().GetTicks();
+ const u32 ticks_per_second = m_system.GetSystemTimers().GetTicksPerSecond();
+ if (u32(current_time - m_timer_button_combo_start) > ticks_per_second * 3)
+ {
+ if (m_last_button_combo == COMBO_RESET)
+ {
+ INFO_LOG_FMT(SERIALINTERFACE, "PAD - COMBO_RESET");
+ m_system.GetProcessorInterface().ResetButton_Tap();
+ }
+ else if (m_last_button_combo == COMBO_ORIGIN)
+ {
+ INFO_LOG_FMT(SERIALINTERFACE, "PAD - COMBO_ORIGIN");
+ SetOrigin(pad_status);
+ }
+
+ m_last_button_combo = COMBO_NONE;
+ return temp_combo;
+ }
+ }
+
+ return COMBO_NONE;
+}
+
+// GetData
+
+// Return true on new data (max 7 Bytes and 6 bits ;)
+// [00?SYXBA] [1LRZUDRL] [x] [y] [cx] [cy] [l] [r]
+// |\_ ERR_LATCH (error latched - check SISR)
+// |_ ERR_STATUS (error on last GetData or SendCmd?)
+DataResponse CSIDevice_AMBaseboard::GetData(u32& hi, u32& low)
+{
+ GCPadStatus pad_status = GetPadStatus();
+
+ if (!pad_status.isConnected)
+ return DataResponse::ErrorNoResponse;
+
+ if (HandleButtonCombos(pad_status) == COMBO_ORIGIN)
+ pad_status.button |= PAD_GET_ORIGIN;
+
+ hi = MapPadStatus(pad_status);
+
+ // Low bits are packed differently per mode
+ if (m_mode == 0 || m_mode == 5 || m_mode == 6 || m_mode == 7)
+ {
+ low = (pad_status.analogB >> 4); // Top 4 bits
+ low |= ((pad_status.analogA >> 4) << 4); // Top 4 bits
+ low |= ((pad_status.triggerRight >> 4) << 8); // Top 4 bits
+ low |= ((pad_status.triggerLeft >> 4) << 12); // Top 4 bits
+ low |= ((pad_status.substickY) << 16); // All 8 bits
+ low |= ((pad_status.substickX) << 24); // All 8 bits
+ }
+ else if (m_mode == 1)
+ {
+ low = (pad_status.analogB >> 4); // Top 4 bits
+ low |= ((pad_status.analogA >> 4) << 4); // Top 4 bits
+ low |= (pad_status.triggerRight << 8); // All 8 bits
+ low |= (pad_status.triggerLeft << 16); // All 8 bits
+ low |= ((pad_status.substickY >> 4) << 24); // Top 4 bits
+ low |= ((pad_status.substickX >> 4) << 28); // Top 4 bits
+ }
+ else if (m_mode == 2)
+ {
+ low = pad_status.analogB; // All 8 bits
+ low |= pad_status.analogA << 8; // All 8 bits
+ low |= ((pad_status.triggerRight >> 4) << 16); // Top 4 bits
+ low |= ((pad_status.triggerLeft >> 4) << 20); // Top 4 bits
+ low |= ((pad_status.substickY >> 4) << 24); // Top 4 bits
+ low |= ((pad_status.substickX >> 4) << 28); // Top 4 bits
+ }
+ else if (m_mode == 3)
+ {
+ // Analog A/B are always 0
+ low = pad_status.triggerRight; // All 8 bits
+ low |= (pad_status.triggerLeft << 8); // All 8 bits
+ low |= (pad_status.substickY << 16); // All 8 bits
+ low |= (pad_status.substickX << 24); // All 8 bits
+ }
+ else if (m_mode == 4)
+ {
+ low = pad_status.analogB; // All 8 bits
+ low |= pad_status.analogA << 8; // All 8 bits
+ // triggerLeft/Right are always 0
+ low |= pad_status.substickY << 16; // All 8 bits
+ low |= pad_status.substickX << 24; // All 8 bits
+ }
+
+ return DataResponse::Success;
+}
+
+void CSIDevice_AMBaseboard::SendCommand(u32 command, u8 poll)
+{
+ UCommand controller_command(command);
+
+ if (static_cast<EDirectCommands>(controller_command.command) == EDirectCommands::CMD_WRITE)
+ {
+ const u32 type = controller_command.parameter1; // 0 = stop, 1 = rumble, 2 = stop hard
+
+ // get the correct pad number that should rumble locally when using netplay
+ const int pad_num = NetPlay_InGamePadToLocalPad(m_device_number);
+
+ if (pad_num < 4)
+ {
+ const SIDevices device = m_system.GetSerialInterface().GetDeviceType(pad_num);
+ if (type == 1)
+ CSIDevice_GCController::Rumble(pad_num, 1.0, device);
+ else
+ CSIDevice_GCController::Rumble(pad_num, 0.0, device);
+ }
+
+ if (poll == 0)
+ {
+ m_mode = controller_command.parameter2;
+ INFO_LOG_FMT(SERIALINTERFACE, "PAD {} set to mode {}", m_device_number, m_mode);
+ }
+ }
+ else if (controller_command.command != 0x00)
+ {
+ // Costis sent 0x00 in some demos :)
+ ERROR_LOG_FMT(SERIALINTERFACE, "Unknown direct command ({:#x})", command);
+ PanicAlertFmt("SI: Unknown direct command");
+ }
+}
+
+void CSIDevice_AMBaseboard::SetOrigin(const GCPadStatus& pad_status)
+{
+ m_origin.origin_stick_x = pad_status.stickX;
+ m_origin.origin_stick_y = pad_status.stickY;
+ m_origin.substick_x = pad_status.substickX;
+ m_origin.substick_y = pad_status.substickY;
+ m_origin.trigger_left = pad_status.triggerLeft;
+ m_origin.trigger_right = pad_status.triggerRight;
+}
+
+void CSIDevice_AMBaseboard::HandleMoviePadStatus(Movie::MovieManager& movie, int device_number,
+ GCPadStatus* pad_status)
+{
+ movie.SetPolledDevice();
+ if (NetPlay_GetInput(device_number, pad_status))
+ {
+ }
+ else if (movie.IsPlayingInput())
+ {
+ movie.PlayController(pad_status, device_number);
+ movie.InputUpdate();
+ }
+ else if (movie.IsRecordingInput())
+ {
+ movie.RecordInput(pad_status, device_number);
+ movie.InputUpdate();
+ }
+ else
+ {
+ movie.CheckPadStatus(pad_status, device_number);
+ }
+}
+
+GCPadStatus CSIDevice_AMBaseboard::GetPadStatus()
+{
+ GCPadStatus pad_status = {};
+
+ // For netplay, the local controllers are polled in GetNetPads(), and
+ // the remote controllers receive their status there as well
+ if (!NetPlay::IsNetPlayRunning())
+ {
+ pad_status = Pad::GetStatus(m_device_number);
+ }
+
+ // Our GCAdapter code sets PAD_GET_ORIGIN when a new device has been connected.
+ // Watch for this to calibrate real controllers on connection.
+ if (pad_status.button & PAD_GET_ORIGIN)
+ SetOrigin(pad_status);
+
+ return pad_status;
+}
+
+void CSIDevice_AMBaseboard::DoState(PointerWrap& p)
+{
+ p.Do(m_origin);
+ p.Do(m_mode);
+ p.Do(m_timer_button_combo_start);
+ p.Do(m_last_button_combo);
+
+ p.Do(m_last);
+ p.Do(m_lastptr);
+
+ p.Do(m_coin);
+ p.Do(m_coin_pressed);
+
+ p.Do(m_ic_card_data);
+
+ // Setup IC-card
+ p.Do(m_ic_card_state);
+ p.Do(m_ic_card_status);
+ p.Do(m_ic_card_session);
+
+ p.Do(m_ic_write_buffer);
+ p.Do(m_ic_write_offset);
+ p.Do(m_ic_write_size);
+
+ // Magnetic Card Reader
+ if (m_mag_card_reader)
+ {
+ m_mag_card_reader->DoState(p);
+
+ p.Do(m_mag_card_in_buffer);
+ p.Do(m_mag_card_out_buffer);
+ }
+
+ // Serial
+ p.Do(m_wheel_init);
+
+ p.Do(m_motor_init);
+ p.Do(m_motor_reply);
+ p.Do(m_motor_force_y);
+
+ // F-Zero AX (DX)
+ p.Do(m_fzdx_seatbelt);
+ p.Do(m_fzdx_motion_stop);
+ p.Do(m_fzdx_sensor_right);
+ p.Do(m_fzdx_sensor_left);
+ p.Do(m_rx_reply);
+
+ // F-Zero AX (CyCraft)
+ p.Do(m_fzcc_seatbelt);
+ p.Do(m_fzcc_sensor);
+ p.Do(m_fzcc_emergency);
+ p.Do(m_fzcc_service);
+
+ p.Do(m_dip_switch_1);
+ p.Do(m_dip_switch_0);
+
+ p.Do(m_delay);
+}
+
+} // namespace SerialInterface
diff --git a/Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.h b/Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.h
new file mode 100644
index 0000000000..31ed04053c
--- /dev/null
+++ b/Source/Core/Core/HW/SI/SI_DeviceAMBaseboard.h
@@ -0,0 +1,315 @@
+// Copyright 2025 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "Core/HW/GCPad.h"
+#include "Core/HW/MagCard/MagneticCardReader.h"
+#include "Core/HW/SI/SI_Device.h"
+
+#include "InputCommon/GCPadStatus.h"
+
+namespace Movie
+{
+class MovieManager;
+}
+
+namespace SerialInterface
+{
+
+// "JAMMA Video Standard" I/O
+class JVSIOMessage
+{
+public:
+ void Start(int node);
+ void AddData(const u8* dst, std::size_t len, int sync);
+ void AddData(const void* data, std::size_t len);
+ void AddData(const char* data);
+ void AddData(u32 n);
+ void End();
+
+ u32 m_pointer = 0;
+ std::array<u8, 0x80> m_message;
+
+private:
+ u32 m_last_start = 0;
+ u32 m_checksum = 0;
+};
+
+// Triforce (GC-AM) baseboard
+class CSIDevice_AMBaseboard : public ISIDevice
+{
+ enum EButtonCombo
+ {
+ COMBO_NONE = 0,
+ COMBO_ORIGIN,
+ COMBO_RESET
+ };
+
+public:
+ // constructor
+ CSIDevice_AMBaseboard(Core::System& system, SIDevices device, int device_number);
+
+ // run the SI Buffer
+ int RunBuffer(u8* buffer, int request_length) override;
+
+ // Reply has to be delayed due a bug in the parser
+ void SwapBuffers(u8* buffer, u32* buffer_length);
+
+ // return true on new data
+ DataResponse GetData(u32& hi, u32& low) override;
+
+ // send a command directly
+ void SendCommand(u32 command, u8 poll) override;
+
+ virtual GCPadStatus GetPadStatus();
+ virtual u32 MapPadStatus(const GCPadStatus& pad_status);
+ virtual EButtonCombo HandleButtonCombos(const GCPadStatus& pad_status);
+
+ static void HandleMoviePadStatus(Movie::MovieManager& movie, int device_number,
+ GCPadStatus* pad_status);
+
+ // Send and Receive pad input from network
+ static bool NetPlay_GetInput(int pad_num, GCPadStatus* status);
+ static int NetPlay_InGamePadToLocalPad(int pad_num);
+
+ void DoState(PointerWrap&) override;
+
+protected:
+ struct SOrigin
+ {
+ u16 button;
+ u8 origin_stick_x;
+ u8 origin_stick_y;
+ u8 substick_x;
+ u8 substick_y;
+ u8 trigger_left;
+ u8 trigger_right;
+ u8 unk_4;
+ u8 unk_5;
+ };
+
+ // struct to compare input against
+ // Set on connection to perfect neutral values
+ // (standard pad only) Set on button combo to current input state
+ SOrigin m_origin = {};
+
+ // PADAnalogMode
+ // Dunno if we need to do this, game/lib should set it?
+ u8 m_mode = 0x3;
+
+ // Timer to track special button combos:
+ // y, X, start for 3 seconds updates origin with current status
+ // Technically, the above is only on standard pad, wavebird does not support it for example
+ // b, x, start for 3 seconds triggers reset (PI reset button interrupt)
+ u64 m_timer_button_combo_start = 0;
+ // Type of button combo from the last/current poll
+ EButtonCombo m_last_button_combo = COMBO_NONE;
+ void SetOrigin(const GCPadStatus& pad_status);
+
+private:
+ enum BaseBoardCommand
+ {
+ GCAM_Reset = 0x00,
+ GCAM_Command = 0x70,
+ };
+
+ enum GCAMCommand
+ {
+ StatusSwitches = 0x10,
+ SerialNumber = 0x11,
+ Unknown_12 = 0x12,
+ Unknown_14 = 0x14,
+ FirmVersion = 0x15,
+ FPGAVersion = 0x16,
+ RegionSettings = 0x1F,
+
+ Unknown_21 = 0x21,
+ Unknown_22 = 0x22,
+ Unknown_23 = 0x23,
+ Unknown_24 = 0x24,
+
+ SerialA = 0x31,
+ SerialB = 0x32,
+
+ JVSIOA = 0x40,
+ JVSIOB = 0x41,
+
+ Unknown_60 = 0x60,
+ };
+
+ enum JVSIOCommand
+ {
+ IOID = 0x10,
+ CommandRevision = 0x11,
+ JVRevision = 0x12,
+ CommunicationVersion = 0x13,
+ CheckFunctionality = 0x14,
+ MainID = 0x15,
+
+ SwitchesInput = 0x20,
+ CoinInput = 0x21,
+ AnalogInput = 0x22,
+ RotaryInput = 0x23,
+ KeyCodeInput = 0x24,
+ PositionInput = 0x25,
+ GeneralSwitchInput = 0x26,
+
+ PayoutRemain = 0x2E,
+ Retrans = 0x2F,
+ CoinSubOutput = 0x30,
+ PayoutAddOutput = 0x31,
+ GeneralDriverOutput = 0x32,
+ AnalogOutput = 0x33,
+ CharacterOutput = 0x34,
+ CoinAddOutput = 0x35,
+ PayoutSubOutput = 0x36,
+ GeneralDriverOutput2 = 0x37,
+ GeneralDriverOutput3 = 0x38,
+
+ NAMCOCommand = 0x70,
+
+ Reset = 0xF0,
+ SetAddress = 0xF1,
+ ChangeComm = 0xF2,
+ };
+
+ enum JVSIOStatusCode
+ {
+ StatusOkay = 1,
+ UnsupportedCommand = 2,
+ ChecksumError = 3,
+ AcknowledgeOverflow = 4,
+ };
+
+ enum CARDCommand
+ {
+ Init = 0x10,
+ GetState = 0x20,
+ Read = 0x33,
+ IsPresent = 0x40,
+ Write = 0x53,
+ SetPrintParam = 0x78,
+ RegisterFont = 0x7A,
+ WriteInfo = 0x7C,
+ Erase = 0x7D,
+ Eject = 0x80,
+ Clean = 0xA0,
+ Load = 0xB0,
+ SetShutter = 0xD0,
+ };
+
+ enum ICCARDCommand
+ {
+ GetStatus = 0x10,
+ SetBaudrate = 0x11,
+ FieldOn = 0x14,
+ FieldOff = 0x15,
+ InsertCheck = 0x20,
+ AntiCollision = 0x21,
+ SelectCard = 0x22,
+ ReadPage = 0x24,
+ WritePage = 0x25,
+ DecreaseUseCount = 0x26,
+ ReadUseCount = 0x33,
+ ReadPages = 0x34,
+ WritePages = 0x35,
+ };
+
+ enum ICCARDStatus
+ {
+ Okay = 0,
+ NoCard = 0x8000,
+ Unknown = 0x800E,
+ BadCard = 0xFFFF,
+ };
+
+ enum CDReaderCommand
+ {
+ ShutterAuto = 0x61,
+ BootVersion = 0x62,
+ SensLock = 0x63,
+ SensCard = 0x65,
+ FirmwareUpdate = 0x66,
+ ShutterGet = 0x67,
+ CameraCheck = 0x68,
+ ShutterCard = 0x69,
+ ProgramChecksum = 0x6B,
+ BootChecksum = 0x6D,
+ ShutterLoad = 0x6F,
+ ReadCard = 0x72,
+ ShutterSave = 0x73,
+ SelfTest = 0x74,
+ ProgramVersion = 0x76,
+ };
+
+ // NOTE: Used to be an union with `u8 data[81 + 4 + 4 + 4]`
+ // TODO: Should the struct be packed?
+ struct ICCommand
+ {
+ u32 pktcmd : 8;
+ u32 pktlen : 8;
+ u32 fixed : 8;
+ u32 command : 8;
+ u32 flag : 8;
+ u32 length : 8;
+ u32 status : 16;
+
+ u8 extdata[81] = {};
+ u32 extlen;
+ };
+
+ u8 m_last[2][0x80] = {};
+ u32 m_lastptr[2] = {};
+
+ std::array<u16, 2> m_coin{};
+ std::array<u32, 2> m_coin_pressed{};
+
+ u8 m_ic_card_data[2048] = {};
+
+ // Setup IC-card
+ u16 m_ic_card_state = 0x20;
+ u16 m_ic_card_status = ICCARDStatus::Okay;
+ u16 m_ic_card_session = 0x23;
+
+ u8 m_ic_write_buffer[512] = {};
+ u32 m_ic_write_offset = 0;
+ u32 m_ic_write_size = 0;
+
+ // Magnetic Card Reader
+ MagCard::MagneticCardReader::Settings m_mag_card_settings;
+
+ std::vector<u8> m_mag_card_in_buffer;
+ std::vector<u8> m_mag_card_out_buffer;
+
+ std::unique_ptr<MagCard::MagneticCardReader> m_mag_card_reader;
+
+ // Serial
+ u32 m_wheel_init = 0;
+
+ u32 m_motor_init = 0;
+ u8 m_motor_reply[64] = {};
+ s16 m_motor_force_y = 0;
+
+ // F-Zero AX (DX)
+ bool m_fzdx_seatbelt = true;
+ bool m_fzdx_motion_stop = false;
+ bool m_fzdx_sensor_right = false;
+ bool m_fzdx_sensor_left = false;
+ u8 m_rx_reply = 0xF0;
+
+ // F-Zero AX (CyCraft)
+ bool m_fzcc_seatbelt = true;
+ bool m_fzcc_sensor = false;
+ bool m_fzcc_emergency = false;
+ bool m_fzcc_service = false;
+
+ u32 m_dip_switch_1 = 0xFE;
+ u32 m_dip_switch_0 = 0xFF;
+
+ int m_delay = 0;
+
+ void ICCardSendReply(ICCommand* iccommand, u8* buffer, u32* length);
+};
+
+} // namespace SerialInterface
diff --git a/Source/Core/Core/HW/VideoInterface.cpp b/Source/Core/Core/HW/VideoInterface.cpp
index af1ebb7bd1..3d46a345ee 100644
--- a/Source/Core/Core/HW/VideoInterface.cpp
+++ b/Source/Core/Core/HW/VideoInterface.cpp
@@ -163,7 +163,9 @@ void VideoInterfaceManager::Preset(bool _bNTSC)
// Say component cable is plugged
m_dtv_status.component_plugged = Config::Get(Config::SYSCONF_PROGRESSIVE_SCAN);
- m_dtv_status.ntsc_j = region == DiscIO::Region::NTSC_J;
+
+ // Triforce IPL requires the DTV NTSC-J flag to be set.
+ m_dtv_status.ntsc_j = m_system.IsTriforce() || region == DiscIO::Region::NTSC_J;
m_fb_width.Hex = 0;
m_border_hblank.Hex = 0;
diff --git a/Source/Core/Core/IOS/Network/Socket.cpp b/Source/Core/Core/IOS/Network/Socket.cpp
index b2f72945e3..6baf371204 100644
--- a/Source/Core/Core/IOS/Network/Socket.cpp
+++ b/Source/Core/Core/IOS/Network/Socket.cpp
@@ -893,11 +893,7 @@ s32 WiiSockMan::AddSocket(s32 fd, bool is_rw)
sock.SetWiiFd(wii_fd);
m_ios.GetSystem().GetPowerPC().GetDebugInterface().NetworkLogger()->OnNewSocket(fd);
-#ifdef __APPLE__
- int opt_no_sigpipe = 1;
- if (setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &opt_no_sigpipe, sizeof(opt_no_sigpipe)) < 0)
- ERROR_LOG_FMT(IOS_NET, "Failed to set SO_NOSIGPIPE on socket");
-#endif
+ Common::SetPlatformSocketOptions(fd);
// Wii UDP sockets can use broadcast address by default
if (!is_rw)
diff --git a/Source/Core/Core/IOS/USB/Emulated/Microphone.h b/Source/Core/Core/IOS/USB/Emulated/Microphone.h
index cf4e2327c1..c2ef1ea031 100644
--- a/Source/Core/Core/IOS/USB/Emulated/Microphone.h
+++ b/Source/Core/Core/IOS/USB/Emulated/Microphone.h
@@ -8,6 +8,7 @@
#include <memory>
#include <mutex>
#include <type_traits>
+#include <vector>
#include "Common/CommonTypes.h"
diff --git a/Source/Core/Core/NetPlayClient.cpp b/Source/Core/Core/NetPlayClient.cpp
index cb6ad2293e..028799d56f 100644
--- a/Source/Core/Core/NetPlayClient.cpp
+++ b/Source/Core/Core/NetPlayClient.cpp
@@ -52,6 +52,7 @@
#include "Core/HW/GCPad.h"
#include "Core/HW/SI/SI.h"
#include "Core/HW/SI/SI_Device.h"
+#include "Core/HW/SI/SI_DeviceAMBaseboard.h"
#include "Core/HW/SI/SI_DeviceGCController.h"
#include "Core/HW/Sram.h"
#include "Core/HW/WiiSave.h"
@@ -1883,6 +1884,8 @@ void NetPlayClient::UpdateDevices()
auto& si = Core::System::GetInstance().GetSerialInterface();
for (auto player_id : m_pad_map)
{
+ const SerialInterface::SIDevices si_device = Config::Get(Config::GetInfoForSIDevice(local_pad));
+
if (m_gba_config[pad].enabled && player_id > 0)
{
si.ChangeDevice(SerialInterface::SIDEVICE_GC_GBA_EMULATED, pad);
@@ -1890,8 +1893,6 @@ void NetPlayClient::UpdateDevices()
else if (player_id == m_local_player->pid)
{
// Use local controller types for local controllers if they are compatible
- const SerialInterface::SIDevices si_device =
- Config::Get(Config::GetInfoForSIDevice(local_pad));
if (SerialInterface::SIDevice_IsGCController(si_device))
{
si.ChangeDevice(si_device, pad);
@@ -1909,7 +1910,8 @@ void NetPlayClient::UpdateDevices()
}
else if (player_id > 0)
{
- si.ChangeDevice(SerialInterface::SIDEVICE_GC_CONTROLLER, pad);
+ if (si_device != SerialInterface::SIDEVICE_AM_BASEBOARD)
+ si.ChangeDevice(SerialInterface::SIDEVICE_GC_CONTROLLER, pad);
}
else
{
@@ -2772,6 +2774,16 @@ bool SerialInterface::CSIDevice_GCController::NetPlay_GetInput(int pad_num, GCPa
return false;
}
+bool SerialInterface::CSIDevice_AMBaseboard::NetPlay_GetInput(int pad_num, GCPadStatus* status)
+{
+ std::lock_guard lk(NetPlay::crit_netplay_client);
+
+ if (NetPlay::netplay_client)
+ return NetPlay::netplay_client->GetNetPads(pad_num, NetPlay::s_si_poll_batching, status);
+
+ return false;
+}
+
bool NetPlay::NetPlay_GetWiimoteData(const std::span<NetPlayClient::WiimoteDataBatchEntry>& entries)
{
std::lock_guard lk(crit_netplay_client);
@@ -2835,12 +2847,21 @@ u64 ExpansionInterface::CEXIIPL::NetPlay_GetEmulatedTime()
// called from ---CPU--- thread
// return the local pad num that should rumble given a ingame pad num
-int SerialInterface::CSIDevice_GCController::NetPlay_InGamePadToLocalPad(int numPAD)
+int SerialInterface::CSIDevice_GCController::NetPlay_InGamePadToLocalPad(int pad_num)
+{
+ std::lock_guard lk(NetPlay::crit_netplay_client);
+
+ if (NetPlay::netplay_client)
+ return NetPlay::netplay_client->InGamePadToLocalPad(pad_num);
+
+ return pad_num;
+}
+int SerialInterface::CSIDevice_AMBaseboard::NetPlay_InGamePadToLocalPad(int pad_num)
{
std::lock_guard lk(NetPlay::crit_netplay_client);
if (NetPlay::netplay_client)
- return NetPlay::netplay_client->InGamePadToLocalPad(numPAD);
+ return NetPlay::netplay_client->InGamePadToLocalPad(pad_num);
- return numPAD;
+ return pad_num;
}
diff --git a/Source/Core/Core/PowerPC/BreakPoints.cpp b/Source/Core/Core/PowerPC/BreakPoints.cpp
index 3cf564663e..e5dca73989 100644
--- a/Source/Core/Core/PowerPC/BreakPoints.cpp
+++ b/Source/Core/Core/PowerPC/BreakPoints.cpp
@@ -34,6 +34,9 @@ bool BreakPoints::IsAddressBreakPoint(u32 address) const
bool BreakPoints::IsBreakPointEnable(u32 address) const
{
+ if (!m_breaking_enabled)
+ return false;
+
const TBreakPoint* bp = GetBreakpoint(address);
return bp != nullptr && bp->is_enabled;
}
@@ -184,6 +187,11 @@ bool BreakPoints::ToggleEnable(u32 address)
return true;
}
+void BreakPoints::EnableBreaking(bool enable)
+{
+ m_breaking_enabled = enable;
+}
+
bool BreakPoints::Remove(u32 address)
{
const auto iter = std::ranges::find(m_breakpoints, address, &TBreakPoint::address);
@@ -318,6 +326,12 @@ bool MemChecks::ToggleEnable(u32 address)
return true;
}
+void MemChecks::EnableBreaking(bool enabled)
+{
+ m_breaking_enabled = enabled;
+ Update();
+}
+
DelayedMemCheckUpdate MemChecks::Remove(u32 address)
{
const auto iter = std::ranges::find(m_mem_checks, address, &TMemCheck::start_address);
diff --git a/Source/Core/Core/PowerPC/BreakPoints.h b/Source/Core/Core/PowerPC/BreakPoints.h
index de22192f7a..da5e755dd3 100644
--- a/Source/Core/Core/PowerPC/BreakPoints.h
+++ b/Source/Core/Core/PowerPC/BreakPoints.h
@@ -87,6 +87,9 @@ public:
bool ToggleBreakPoint(u32 address);
bool ToggleEnable(u32 address);
+ void EnableBreaking(bool enable);
+ bool IsBreakingEnabled() const { return m_breaking_enabled; }
+
// Remove Breakpoint. Returns whether it was removed.
bool Remove(u32 address);
void Clear();
@@ -96,6 +99,7 @@ private:
TBreakPoints m_breakpoints;
std::optional<TBreakPoint> m_temp_breakpoint;
Core::System& m_system;
+ bool m_breaking_enabled = true;
};
class DelayedMemCheckUpdate;
@@ -126,9 +130,12 @@ public:
bool OverlapsMemcheck(u32 address, u32 length) const;
DelayedMemCheckUpdate Remove(u32 address);
+ void EnableBreaking(bool enable);
+ bool IsBreakingEnabled() const { return m_breaking_enabled; }
+
void Update();
void Clear();
- bool HasAny() const { return !m_mem_checks.empty(); }
+ bool HasAny() const { return !m_mem_checks.empty() && m_breaking_enabled; }
BitSet32 GetGPRsUsedInConditions() { return m_gprs_used_in_conditions; }
BitSet32 GetFPRsUsedInConditions() { return m_fprs_used_in_conditions; }
@@ -142,6 +149,7 @@ private:
BitSet32 m_gprs_used_in_conditions;
BitSet32 m_fprs_used_in_conditions;
bool m_mem_breakpoints_set = false;
+ bool m_breaking_enabled = true;
};
class DelayedMemCheckUpdate final
diff --git a/Source/Core/Core/PowerPC/Jit64/Jit.cpp b/Source/Core/Core/PowerPC/Jit64/Jit.cpp
index 78bda8451b..0b976eb1c8 100644
--- a/Source/Core/Core/PowerPC/Jit64/Jit.cpp
+++ b/Source/Core/Core/PowerPC/Jit64/Jit.cpp
@@ -1067,13 +1067,12 @@ bool Jit64::DoJit(u32 em_address, JitBlock* b, u32 nextPC)
if (op.skip)
{
- if (IsDebuggingEnabled())
+ if (IsBranchWatchEnabled())
{
// The only thing that currently sets op.skip is the BLR following optimization.
// If any non-branch instruction starts setting that too, this will need to be changed.
ASSERT(op.inst.hex == 0x4e800020);
- WriteBranchWatch<true>(op.address, op.branchTo, op.inst, RSCRATCH, RSCRATCH2,
- CallerSavedRegistersInUse());
+ WriteBranchWatch<true>(op.address, op.branchTo, op.inst, CallerSavedRegistersInUse());
}
}
else
diff --git a/Source/Core/Core/PowerPC/Jit64/Jit.h b/Source/Core/Core/PowerPC/Jit64/Jit.h
index fde8691b48..2d8ef2be55 100644
--- a/Source/Core/Core/PowerPC/Jit64/Jit.h
+++ b/Source/Core/Core/PowerPC/Jit64/Jit.h
@@ -110,10 +110,8 @@ public:
void WriteRfiExitDestInRSCRATCH();
void WriteIdleExit(u32 destination);
template <bool condition>
- void WriteBranchWatch(u32 origin, u32 destination, UGeckoInstruction inst, Gen::X64Reg reg_a,
- Gen::X64Reg reg_b, BitSet32 caller_save);
- void WriteBranchWatchDestInRSCRATCH(u32 origin, UGeckoInstruction inst, Gen::X64Reg reg_a,
- Gen::X64Reg reg_b, BitSet32 caller_save);
+ void WriteBranchWatch(u32 origin, u32 destination, UGeckoInstruction inst, BitSet32 caller_save);
+ void WriteBranchWatchDestInRSCRATCH(u32 origin, UGeckoInstruction inst, BitSet32 caller_save);
bool Cleanup();
diff --git a/Source/Core/Core/PowerPC/Jit64/Jit_Branch.cpp b/Source/Core/Core/PowerPC/Jit64/Jit_Branch.cpp
index 81e7ad8a09..4667238720 100644
--- a/Source/Core/Core/PowerPC/Jit64/Jit_Branch.cpp
+++ b/Source/Core/Core/PowerPC/Jit64/Jit_Branch.cpp
@@ -67,65 +67,42 @@ void Jit64::rfi(UGeckoInstruction inst)
}
template <bool condition>
-void Jit64::WriteBranchWatch(u32 origin, u32 destination, UGeckoInstruction inst, X64Reg reg_a,
- X64Reg reg_b, BitSet32 caller_save)
+void Jit64::WriteBranchWatch(u32 origin, u32 destination, UGeckoInstruction inst,
+ BitSet32 caller_save)
{
- MOV(64, R(reg_a), ImmPtr(&m_branch_watch));
- MOVZX(32, 8, reg_b, MDisp(reg_a, Core::BranchWatch::GetOffsetOfRecordingActive()));
- TEST(32, R(reg_b), R(reg_b));
-
- FixupBranch branch_in = J_CC(CC_NZ, Jump::Near);
- SwitchToFarCode();
- SetJumpTarget(branch_in);
-
- ABI_PushRegistersAndAdjustStack(caller_save, 0);
- // Some call sites have an optimization to use ABI_PARAM1 as a scratch register.
- if (reg_a != ABI_PARAM1)
- MOV(64, R(ABI_PARAM1), R(reg_a));
- MOV(64, R(ABI_PARAM2), Imm64(Core::FakeBranchWatchCollectionKey{origin, destination}));
- MOV(32, R(ABI_PARAM3), Imm32(inst.hex));
- ABI_CallFunction(m_ppc_state.msr.IR ? (condition ? &Core::BranchWatch::HitVirtualTrue_fk :
- &Core::BranchWatch::HitVirtualFalse_fk) :
- (condition ? &Core::BranchWatch::HitPhysicalTrue_fk :
- &Core::BranchWatch::HitPhysicalFalse_fk));
- ABI_PopRegistersAndAdjustStack(caller_save, 0);
-
- FixupBranch branch_out = J(Jump::Near);
- SwitchToNearCode();
- SetJumpTarget(branch_out);
+ if (IsBranchWatchEnabled())
+ {
+ ABI_PushRegistersAndAdjustStack(caller_save, 0);
+ MOV(64, R(ABI_PARAM1), ImmPtr(&m_branch_watch));
+ MOV(64, R(ABI_PARAM2), Imm64(Core::FakeBranchWatchCollectionKey{origin, destination}));
+ MOV(32, R(ABI_PARAM3), Imm32(inst.hex));
+ ABI_CallFunction(m_ppc_state.msr.IR ? (condition ? &Core::BranchWatch::HitVirtualTrue_fk :
+ &Core::BranchWatch::HitVirtualFalse_fk) :
+ (condition ? &Core::BranchWatch::HitPhysicalTrue_fk :
+ &Core::BranchWatch::HitPhysicalFalse_fk));
+ ABI_PopRegistersAndAdjustStack(caller_save, 0);
+ }
}
-template void Jit64::WriteBranchWatch<true>(u32, u32, UGeckoInstruction, X64Reg, X64Reg, BitSet32);
-template void Jit64::WriteBranchWatch<false>(u32, u32, UGeckoInstruction, X64Reg, X64Reg, BitSet32);
+template void Jit64::WriteBranchWatch<true>(u32, u32, UGeckoInstruction, BitSet32);
+template void Jit64::WriteBranchWatch<false>(u32, u32, UGeckoInstruction, BitSet32);
-void Jit64::WriteBranchWatchDestInRSCRATCH(u32 origin, UGeckoInstruction inst, X64Reg reg_a,
- X64Reg reg_b, BitSet32 caller_save)
+void Jit64::WriteBranchWatchDestInRSCRATCH(u32 origin, UGeckoInstruction inst, BitSet32 caller_save)
{
- MOV(64, R(reg_a), ImmPtr(&m_branch_watch));
- MOVZX(32, 8, reg_b, MDisp(reg_a, Core::BranchWatch::GetOffsetOfRecordingActive()));
- TEST(32, R(reg_b), R(reg_b));
-
- FixupBranch branch_in = J_CC(CC_NZ, Jump::Near);
- SwitchToFarCode();
- SetJumpTarget(branch_in);
-
- // Assert RSCRATCH won't be clobbered before it is moved from.
- static_assert(ABI_PARAM1 != RSCRATCH);
-
- ABI_PushRegistersAndAdjustStack(caller_save, 0);
- // Some call sites have an optimization to use ABI_PARAM1 as a scratch register.
- if (reg_a != ABI_PARAM1)
- MOV(64, R(ABI_PARAM1), R(reg_a));
- MOV(32, R(ABI_PARAM3), R(RSCRATCH));
- MOV(32, R(ABI_PARAM2), Imm32(origin));
- MOV(32, R(ABI_PARAM4), Imm32(inst.hex));
- ABI_CallFunction(m_ppc_state.msr.IR ? &Core::BranchWatch::HitVirtualTrue :
- &Core::BranchWatch::HitPhysicalTrue);
- ABI_PopRegistersAndAdjustStack(caller_save, 0);
-
- FixupBranch branch_out = J(Jump::Near);
- SwitchToNearCode();
- SetJumpTarget(branch_out);
+ if (IsBranchWatchEnabled())
+ {
+ // Assert RSCRATCH won't be clobbered before it is moved from.
+ static_assert(ABI_PARAM1 != RSCRATCH);
+
+ ABI_PushRegistersAndAdjustStack(caller_save, 0);
+ MOV(64, R(ABI_PARAM1), ImmPtr(&m_branch_watch));
+ MOV(32, R(ABI_PARAM3), R(RSCRATCH));
+ MOV(32, R(ABI_PARAM2), Imm32(origin));
+ MOV(32, R(ABI_PARAM4), Imm32(inst.hex));
+ ABI_CallFunction(m_ppc_state.msr.IR ? &Core::BranchWatch::HitVirtualTrue :
+ &Core::BranchWatch::HitPhysicalTrue);
+ ABI_PopRegistersAndAdjustStack(caller_save, 0);
+ }
}
void Jit64::bx(UGeckoInstruction inst)
@@ -143,11 +120,7 @@ void Jit64::bx(UGeckoInstruction inst)
// Because PPCAnalyst::Flatten() merged the blocks.
if (!js.isLastInstruction)
{
- if (IsDebuggingEnabled())
- {
- WriteBranchWatch<true>(js.compilerPC, js.op->branchTo, inst, RSCRATCH, RSCRATCH2,
- CallerSavedRegistersInUse());
- }
+ WriteBranchWatch<true>(js.compilerPC, js.op->branchTo, inst, CallerSavedRegistersInUse());
if (inst.LK && !js.op->skipLRStack)
{
// We have to fake the stack as the RET instruction was not
@@ -161,11 +134,7 @@ void Jit64::bx(UGeckoInstruction inst)
gpr.Flush();
fpr.Flush();
- if (IsDebuggingEnabled())
- {
- // ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function.
- WriteBranchWatch<true>(js.compilerPC, js.op->branchTo, inst, ABI_PARAM1, RSCRATCH, {});
- }
+ WriteBranchWatch<true>(js.compilerPC, js.op->branchTo, inst, {});
#ifdef ACID_TEST
if (inst.LK)
AND(32, PPCSTATE(cr), Imm32(~(0xFF000000)));
@@ -216,11 +185,7 @@ void Jit64::bcx(UGeckoInstruction inst)
if (!js.isLastInstruction && (inst.BO & BO_DONT_DECREMENT_FLAG) &&
(inst.BO & BO_DONT_CHECK_CONDITION))
{
- if (IsDebuggingEnabled())
- {
- WriteBranchWatch<true>(js.compilerPC, js.op->branchTo, inst, RSCRATCH, RSCRATCH2,
- CallerSavedRegistersInUse());
- }
+ WriteBranchWatch<true>(js.compilerPC, js.op->branchTo, inst, CallerSavedRegistersInUse());
if (inst.LK && !js.op->skipLRStack)
{
// We have to fake the stack as the RET instruction was not
@@ -237,11 +202,7 @@ void Jit64::bcx(UGeckoInstruction inst)
gpr.Flush();
fpr.Flush();
- if (IsDebuggingEnabled())
- {
- // ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function.
- WriteBranchWatch<true>(js.compilerPC, js.op->branchTo, inst, ABI_PARAM1, RSCRATCH, {});
- }
+ WriteBranchWatch<true>(js.compilerPC, js.op->branchTo, inst, {});
if (js.op->branchIsIdleLoop)
{
WriteIdleExit(js.op->branchTo);
@@ -261,18 +222,10 @@ void Jit64::bcx(UGeckoInstruction inst)
{
gpr.Flush();
fpr.Flush();
- if (IsDebuggingEnabled())
- {
- // ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function.
- WriteBranchWatch<false>(js.compilerPC, js.compilerPC + 4, inst, ABI_PARAM1, RSCRATCH, {});
- }
+ WriteBranchWatch<false>(js.compilerPC, js.compilerPC + 4, inst, {});
WriteExit(js.compilerPC + 4);
}
- else if (IsDebuggingEnabled())
- {
- WriteBranchWatch<false>(js.compilerPC, js.compilerPC + 4, inst, RSCRATCH, RSCRATCH2,
- CallerSavedRegistersInUse());
- }
+ WriteBranchWatch<false>(js.compilerPC, js.compilerPC + 4, inst, CallerSavedRegistersInUse());
}
void Jit64::bcctrx(UGeckoInstruction inst)
@@ -296,12 +249,7 @@ void Jit64::bcctrx(UGeckoInstruction inst)
if (inst.LK_3)
MOV(32, PPCSTATE_LR, Imm32(js.compilerPC + 4)); // LR = PC + 4;
AND(32, R(RSCRATCH), Imm32(0xFFFFFFFC));
- if (IsDebuggingEnabled())
- {
- // ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function.
- WriteBranchWatchDestInRSCRATCH(js.compilerPC, inst, ABI_PARAM1, RSCRATCH2,
- BitSet32{RSCRATCH});
- }
+ WriteBranchWatchDestInRSCRATCH(js.compilerPC, inst, BitSet32{RSCRATCH});
WriteExitDestInRSCRATCH(inst.LK_3, js.compilerPC + 4);
}
else
@@ -324,12 +272,7 @@ void Jit64::bcctrx(UGeckoInstruction inst)
RCForkGuard fpr_guard = fpr.Fork();
gpr.Flush();
fpr.Flush();
- if (IsDebuggingEnabled())
- {
- // ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function.
- WriteBranchWatchDestInRSCRATCH(js.compilerPC, inst, ABI_PARAM1, RSCRATCH2,
- BitSet32{RSCRATCH});
- }
+ WriteBranchWatchDestInRSCRATCH(js.compilerPC, inst, BitSet32{RSCRATCH});
WriteExitDestInRSCRATCH(inst.LK_3, js.compilerPC + 4);
// Would really like to continue the block here, but it ends. TODO.
}
@@ -339,18 +282,10 @@ void Jit64::bcctrx(UGeckoInstruction inst)
{
gpr.Flush();
fpr.Flush();
- if (IsDebuggingEnabled())
- {
- // ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function.
- WriteBranchWatch<false>(js.compilerPC, js.compilerPC + 4, inst, ABI_PARAM1, RSCRATCH, {});
- }
+ WriteBranchWatch<false>(js.compilerPC, js.compilerPC + 4, inst, {});
WriteExit(js.compilerPC + 4);
}
- else if (IsDebuggingEnabled())
- {
- WriteBranchWatch<false>(js.compilerPC, js.compilerPC + 4, inst, RSCRATCH, RSCRATCH2,
- CallerSavedRegistersInUse());
- }
+ WriteBranchWatch<false>(js.compilerPC, js.compilerPC + 4, inst, CallerSavedRegistersInUse());
}
}
@@ -399,21 +334,12 @@ void Jit64::bclrx(UGeckoInstruction inst)
if (js.op->branchIsIdleLoop)
{
- if (IsDebuggingEnabled())
- {
- // ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function.
- WriteBranchWatch<true>(js.compilerPC, js.op->branchTo, inst, ABI_PARAM1, RSCRATCH, {});
- }
+ WriteBranchWatch<true>(js.compilerPC, js.op->branchTo, inst, {});
WriteIdleExit(js.op->branchTo);
}
else
{
- if (IsDebuggingEnabled())
- {
- // ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function.
- WriteBranchWatchDestInRSCRATCH(js.compilerPC, inst, ABI_PARAM1, RSCRATCH2,
- BitSet32{RSCRATCH});
- }
+ WriteBranchWatchDestInRSCRATCH(js.compilerPC, inst, BitSet32{RSCRATCH});
WriteBLRExit();
}
}
@@ -427,16 +353,11 @@ void Jit64::bclrx(UGeckoInstruction inst)
{
gpr.Flush();
fpr.Flush();
- if (IsDebuggingEnabled())
- {
- // ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function.
- WriteBranchWatch<false>(js.compilerPC, js.compilerPC + 4, inst, ABI_PARAM1, RSCRATCH, {});
- }
+ WriteBranchWatch<false>(js.compilerPC, js.compilerPC + 4, inst, {});
WriteExit(js.compilerPC + 4);
}
- else if (IsDebuggingEnabled())
+ else
{
- WriteBranchWatch<false>(js.compilerPC, js.compilerPC + 4, inst, RSCRATCH, RSCRATCH2,
- CallerSavedRegistersInUse());
+ WriteBranchWatch<false>(js.compilerPC, js.compilerPC + 4, inst, CallerSavedRegistersInUse());
}
}
diff --git a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp
index 90fd009787..f3c329cef4 100644
--- a/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp
+++ b/Source/Core/Core/PowerPC/Jit64/Jit_Integer.cpp
@@ -387,11 +387,7 @@ void Jit64::DoMergedBranch()
MOV(32, PPCSTATE_SPR(SPR_LR), Imm32(nextPC + 4));
const u32 destination = js.op[1].branchTo;
- if (IsDebuggingEnabled())
- {
- // ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function.
- WriteBranchWatch<true>(nextPC, destination, next, ABI_PARAM1, RSCRATCH, {});
- }
+ WriteBranchWatch<true>(nextPC, destination, next, {});
WriteIdleExit(destination);
}
else if (next.OPCD == 16) // bcx
@@ -400,11 +396,7 @@ void Jit64::DoMergedBranch()
MOV(32, PPCSTATE_SPR(SPR_LR), Imm32(nextPC + 4));
const u32 destination = js.op[1].branchTo;
- if (IsDebuggingEnabled())
- {
- // ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function.
- WriteBranchWatch<true>(nextPC, destination, next, ABI_PARAM1, RSCRATCH, {});
- }
+ WriteBranchWatch<true>(nextPC, destination, next, {});
WriteExit(destination, next.LK, nextPC + 4);
}
else if ((next.OPCD == 19) && (next.SUBOP10 == 528)) // bcctrx
@@ -413,11 +405,7 @@ void Jit64::DoMergedBranch()
MOV(32, PPCSTATE_SPR(SPR_LR), Imm32(nextPC + 4));
MOV(32, R(RSCRATCH), PPCSTATE_SPR(SPR_CTR));
AND(32, R(RSCRATCH), Imm32(0xFFFFFFFC));
- if (IsDebuggingEnabled())
- {
- // ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function.
- WriteBranchWatchDestInRSCRATCH(nextPC, next, ABI_PARAM1, RSCRATCH2, BitSet32{RSCRATCH});
- }
+ WriteBranchWatchDestInRSCRATCH(nextPC, next, BitSet32{RSCRATCH});
WriteExitDestInRSCRATCH(next.LK, nextPC + 4);
}
else if ((next.OPCD == 19) && (next.SUBOP10 == 16)) // bclrx
@@ -427,11 +415,7 @@ void Jit64::DoMergedBranch()
AND(32, R(RSCRATCH), Imm32(0xFFFFFFFC));
if (next.LK)
MOV(32, PPCSTATE_SPR(SPR_LR), Imm32(nextPC + 4));
- if (IsDebuggingEnabled())
- {
- // ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function.
- WriteBranchWatchDestInRSCRATCH(nextPC, next, ABI_PARAM1, RSCRATCH2, BitSet32{RSCRATCH});
- }
+ WriteBranchWatchDestInRSCRATCH(nextPC, next, BitSet32{RSCRATCH});
WriteBLRExit();
}
else
@@ -488,17 +472,12 @@ void Jit64::DoMergedBranchCondition()
{
gpr.Flush();
fpr.Flush();
- if (IsDebuggingEnabled())
- {
- // ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function.
- WriteBranchWatch<false>(nextPC, nextPC + 4, next, ABI_PARAM1, RSCRATCH, {});
- }
+ WriteBranchWatch<false>(nextPC, nextPC + 4, next, {});
WriteExit(nextPC + 4);
}
- else if (IsDebuggingEnabled())
+ else
{
- WriteBranchWatch<false>(nextPC, nextPC + 4, next, RSCRATCH, RSCRATCH2,
- CallerSavedRegistersInUse());
+ WriteBranchWatch<false>(nextPC, nextPC + 4, next, CallerSavedRegistersInUse());
}
}
@@ -540,17 +519,12 @@ void Jit64::DoMergedBranchImmediate(s64 val)
{
gpr.Flush();
fpr.Flush();
- if (IsDebuggingEnabled())
- {
- // ABI_PARAM1 is safe to use after a GPR flush for an optimization in this function.
- WriteBranchWatch<false>(nextPC, nextPC + 4, next, ABI_PARAM1, RSCRATCH, {});
- }
+ WriteBranchWatch<false>(nextPC, nextPC + 4, next, {});
WriteExit(nextPC + 4);
}
- else if (IsDebuggingEnabled())
+ else
{
- WriteBranchWatch<false>(nextPC, nextPC + 4, next, RSCRATCH, RSCRATCH2,
- CallerSavedRegistersInUse());
+ WriteBranchWatch<false>(nextPC, nextPC + 4, next, CallerSavedRegistersInUse());
}
}
diff --git a/Source/Core/Core/PowerPC/Jit64/Jit_LoadStore.cpp b/Source/Core/Core/PowerPC/Jit64/Jit_LoadStore.cpp
index 4b11f96942..b4a3b4ef43 100644
--- a/Source/Core/Core/PowerPC/Jit64/Jit_LoadStore.cpp
+++ b/Source/Core/Core/PowerPC/Jit64/Jit_LoadStore.cpp
@@ -305,25 +305,16 @@ void Jit64::dcbx(UGeckoInstruction inst)
// Load the loop_counter register with the amount of invalidations to execute.
LEA(32, loop_counter, MDisp(RSCRATCH2, 1));
- if (IsDebuggingEnabled())
+ if (IsBranchWatchEnabled())
{
- const X64Reg bw_reg_a = reg_cycle_count, bw_reg_b = reg_downcount;
const BitSet32 bw_caller_save = (CallerSavedRegistersInUse() | BitSet32{RSCRATCH2}) &
- ~BitSet32{int(bw_reg_a), int(bw_reg_b)};
-
- MOV(64, R(bw_reg_a), ImmPtr(&m_branch_watch));
- MOVZX(32, 8, bw_reg_b, MDisp(bw_reg_a, Core::BranchWatch::GetOffsetOfRecordingActive()));
- TEST(32, R(bw_reg_b), R(bw_reg_b));
-
- FixupBranch branch_in = J_CC(CC_NZ, Jump::Near);
- SwitchToFarCode();
- SetJumpTarget(branch_in);
+ ~BitSet32{int(reg_cycle_count), int(reg_downcount)};
// Assert RSCRATCH2 won't be clobbered before it is moved from.
static_assert(RSCRATCH2 != ABI_PARAM1);
ABI_PushRegistersAndAdjustStack(bw_caller_save, 0);
- MOV(64, R(ABI_PARAM1), R(bw_reg_a));
+ MOV(64, R(ABI_PARAM1), ImmPtr(&m_branch_watch));
// RSCRATCH2 holds the amount of faked branch watch hits. Move RSCRATCH2 first, because
// ABI_PARAM2 clobbers RSCRATCH2 on Windows and ABI_PARAM3 clobbers RSCRATCH2 on Linux!
MOV(32, R(ABI_PARAM4), R(RSCRATCH2));
@@ -333,10 +324,6 @@ void Jit64::dcbx(UGeckoInstruction inst)
ABI_CallFunction(m_ppc_state.msr.IR ? &Core::BranchWatch::HitVirtualTrue_fk_n :
&Core::BranchWatch::HitPhysicalTrue_fk_n);
ABI_PopRegistersAndAdjustStack(bw_caller_save, 0);
-
- FixupBranch branch_out = J(Jump::Near);
- SwitchToNearCode();
- SetJumpTarget(branch_out);
}
}
diff --git a/Source/Core/Core/PowerPC/JitArm64/Jit.cpp b/Source/Core/Core/PowerPC/JitArm64/Jit.cpp
index c04c45783d..ce5dd96c74 100644
--- a/Source/Core/Core/PowerPC/JitArm64/Jit.cpp
+++ b/Source/Core/Core/PowerPC/JitArm64/Jit.cpp
@@ -1296,16 +1296,13 @@ bool JitArm64::DoJit(u32 em_address, JitBlock* b, u32 nextPC)
if (op.skip)
{
- if (IsDebuggingEnabled())
+ if (IsBranchWatchEnabled())
{
// The only thing that currently sets op.skip is the BLR following optimization.
// If any non-branch instruction starts setting that too, this will need to be changed.
ASSERT(op.inst.hex == 0x4e800020);
- const auto bw_reg_a = gpr.GetScopedReg(), bw_reg_b = gpr.GetScopedReg();
- const BitSet32 gpr_caller_save =
- gpr.GetCallerSavedUsed() & ~BitSet32{DecodeReg(bw_reg_a), DecodeReg(bw_reg_b)};
- WriteBranchWatch<true>(op.address, op.branchTo, op.inst, bw_reg_a, bw_reg_b,
- gpr_caller_save, fpr.GetCallerSavedUsed());
+ WriteBranchWatch<true>(op.address, op.branchTo, op.inst, gpr.GetCallerSavedUsed(),
+ fpr.GetCallerSavedUsed());
}
}
else
diff --git a/Source/Core/Core/PowerPC/JitArm64/Jit.h b/Source/Core/Core/PowerPC/JitArm64/Jit.h
index 7a46eb58fe..5b42dff3dd 100644
--- a/Source/Core/Core/PowerPC/JitArm64/Jit.h
+++ b/Source/Core/Core/PowerPC/JitArm64/Jit.h
@@ -334,11 +334,9 @@ protected:
// Branch Watch
template <bool condition>
void WriteBranchWatch(u32 origin, u32 destination, UGeckoInstruction inst,
- Arm64Gen::ARM64Reg reg_a, Arm64Gen::ARM64Reg reg_b,
BitSet32 gpr_caller_save, BitSet32 fpr_caller_save);
void WriteBranchWatchDestInRegister(u32 origin, Arm64Gen::ARM64Reg destination,
- UGeckoInstruction inst, Arm64Gen::ARM64Reg reg_a,
- Arm64Gen::ARM64Reg reg_b, BitSet32 gpr_caller_save,
+ UGeckoInstruction inst, BitSet32 gpr_caller_save,
BitSet32 fpr_caller_save);
// Exits
diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Branch.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Branch.cpp
index a9df905b10..191c423876 100644
--- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_Branch.cpp
+++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_Branch.cpp
@@ -75,67 +75,35 @@ void JitArm64::rfi(UGeckoInstruction inst)
}
template <bool condition>
-void JitArm64::WriteBranchWatch(u32 origin, u32 destination, UGeckoInstruction inst, ARM64Reg reg_a,
- ARM64Reg reg_b, BitSet32 gpr_caller_save, BitSet32 fpr_caller_save)
+void JitArm64::WriteBranchWatch(u32 origin, u32 destination, UGeckoInstruction inst,
+ BitSet32 gpr_caller_save, BitSet32 fpr_caller_save)
{
- const ARM64Reg branch_watch = EncodeRegTo64(reg_a);
- MOVP2R(branch_watch, &m_branch_watch);
- LDRB(IndexType::Unsigned, reg_b, branch_watch, Core::BranchWatch::GetOffsetOfRecordingActive());
- FixupBranch branch_over = CBZ(reg_b);
-
- FixupBranch branch_in = B();
- SwitchToFarCode();
- SetJumpTarget(branch_in);
-
- const ARM64Reg float_emit_tmp = EncodeRegTo64(reg_b);
ABI_PushRegisters(gpr_caller_save);
- m_float_emit.ABI_PushRegisters(fpr_caller_save, float_emit_tmp);
+ m_float_emit.ABI_PushRegisters(fpr_caller_save, ARM64Reg::X8);
ABI_CallFunction(m_ppc_state.msr.IR ? (condition ? &Core::BranchWatch::HitVirtualTrue_fk :
&Core::BranchWatch::HitVirtualFalse_fk) :
(condition ? &Core::BranchWatch::HitPhysicalTrue_fk :
&Core::BranchWatch::HitPhysicalFalse_fk),
- branch_watch, Core::FakeBranchWatchCollectionKey{origin, destination}, inst.hex);
- m_float_emit.ABI_PopRegisters(fpr_caller_save, float_emit_tmp);
+ &m_branch_watch, Core::FakeBranchWatchCollectionKey{origin, destination},
+ inst.hex);
+ m_float_emit.ABI_PopRegisters(fpr_caller_save, ARM64Reg::X8);
ABI_PopRegisters(gpr_caller_save);
-
- FixupBranch branch_out = B();
- SwitchToNearCode();
- SetJumpTarget(branch_out);
- SetJumpTarget(branch_over);
}
-template void JitArm64::WriteBranchWatch<true>(u32, u32, UGeckoInstruction, ARM64Reg, ARM64Reg,
- BitSet32, BitSet32);
-template void JitArm64::WriteBranchWatch<false>(u32, u32, UGeckoInstruction, ARM64Reg, ARM64Reg,
- BitSet32, BitSet32);
+template void JitArm64::WriteBranchWatch<true>(u32, u32, UGeckoInstruction, BitSet32, BitSet32);
+template void JitArm64::WriteBranchWatch<false>(u32, u32, UGeckoInstruction, BitSet32, BitSet32);
void JitArm64::WriteBranchWatchDestInRegister(u32 origin, ARM64Reg destination,
- UGeckoInstruction inst, ARM64Reg reg_a,
- ARM64Reg reg_b, BitSet32 gpr_caller_save,
+ UGeckoInstruction inst, BitSet32 gpr_caller_save,
BitSet32 fpr_caller_save)
{
- const ARM64Reg branch_watch = EncodeRegTo64(reg_a);
- MOVP2R(branch_watch, &m_branch_watch);
- LDRB(IndexType::Unsigned, reg_b, branch_watch, Core::BranchWatch::GetOffsetOfRecordingActive());
- FixupBranch branch_over = CBZ(reg_b);
-
- FixupBranch branch_in = B();
- SwitchToFarCode();
- SetJumpTarget(branch_in);
-
- const ARM64Reg float_emit_tmp = EncodeRegTo64(reg_b);
ABI_PushRegisters(gpr_caller_save);
- m_float_emit.ABI_PushRegisters(fpr_caller_save, float_emit_tmp);
+ m_float_emit.ABI_PushRegisters(fpr_caller_save, ARM64Reg::X8);
ABI_CallFunction(m_ppc_state.msr.IR ? &Core::BranchWatch::HitVirtualTrue :
&Core::BranchWatch::HitPhysicalTrue,
- branch_watch, origin, destination, inst.hex);
- m_float_emit.ABI_PopRegisters(fpr_caller_save, float_emit_tmp);
+ &m_branch_watch, origin, destination, inst.hex);
+ m_float_emit.ABI_PopRegisters(fpr_caller_save, ARM64Reg::X8);
ABI_PopRegisters(gpr_caller_save);
-
- FixupBranch branch_out = B();
- SwitchToNearCode();
- SetJumpTarget(branch_out);
- SetJumpTarget(branch_over);
}
void JitArm64::bx(UGeckoInstruction inst)
@@ -153,13 +121,12 @@ void JitArm64::bx(UGeckoInstruction inst)
if (!js.isLastInstruction)
{
- if (IsDebuggingEnabled())
+ if (IsBranchWatchEnabled())
{
- const auto WB = gpr.GetScopedReg(), WC = gpr.GetScopedReg();
- BitSet32 gpr_caller_save = gpr.GetCallerSavedUsed() & ~BitSet32{DecodeReg(WB), DecodeReg(WC)};
+ BitSet32 gpr_caller_save = gpr.GetCallerSavedUsed();
if (WA != ARM64Reg::INVALID_REG && js.op->skipLRStack)
gpr_caller_save[DecodeReg(WA)] = false;
- WriteBranchWatch<true>(js.compilerPC, js.op->branchTo, inst, WB, WC, gpr_caller_save,
+ WriteBranchWatch<true>(js.compilerPC, js.op->branchTo, inst, gpr_caller_save,
fpr.GetCallerSavedUsed());
}
if (inst.LK && !js.op->skipLRStack)
@@ -178,15 +145,14 @@ void JitArm64::bx(UGeckoInstruction inst)
if (js.op->branchIsIdleLoop)
{
- if (WA == ARM64Reg::INVALID_REG)
- WA = gpr.GetScopedReg();
-
- if (IsDebuggingEnabled())
+ if (IsBranchWatchEnabled())
{
- const auto WB = gpr.GetScopedReg();
- WriteBranchWatch<true>(js.compilerPC, js.op->branchTo, inst, WA, WB, {}, {});
+ WriteBranchWatch<true>(js.compilerPC, js.op->branchTo, inst, {}, {});
}
+ if (WA == ARM64Reg::INVALID_REG)
+ WA = gpr.GetScopedReg();
+
// make idle loops go faster
ARM64Reg XA = EncodeRegTo64(WA);
@@ -198,12 +164,11 @@ void JitArm64::bx(UGeckoInstruction inst)
return;
}
- if (IsDebuggingEnabled())
+ if (IsBranchWatchEnabled())
{
- const auto WB = gpr.GetScopedReg(), WC = gpr.GetScopedReg();
const BitSet32 gpr_caller_save =
WA != ARM64Reg::INVALID_REG ? BitSet32{DecodeReg(WA)} & CALLER_SAVED_GPRS : BitSet32{};
- WriteBranchWatch<true>(js.compilerPC, js.op->branchTo, inst, WB, WC, gpr_caller_save, {});
+ WriteBranchWatch<true>(js.compilerPC, js.op->branchTo, inst, gpr_caller_save, {});
}
WriteExit(js.op->branchTo, inst.LK, js.compilerPC + 4, WA);
}
@@ -214,14 +179,11 @@ void JitArm64::bcx(UGeckoInstruction inst)
JITDISABLE(bJITBranchOff);
auto WA = gpr.GetScopedReg();
- auto WB = inst.LK || IsDebuggingEnabled() ? gpr.GetScopedReg() :
- Arm64GPRCache::ScopedARM64Reg(WA.GetReg());
+ // If WA isn't needed for WriteExit, it can be safely clobbered.
+ auto WB = (inst.LK && !js.op->branchIsIdleLoop) ? gpr.GetScopedReg() :
+ Arm64GPRCache::ScopedARM64Reg(WA.GetReg());
{
- auto WC = IsDebuggingEnabled() && inst.LK && !js.op->branchIsIdleLoop ?
- gpr.GetScopedReg() :
- Arm64GPRCache::ScopedARM64Reg(ARM64Reg::INVALID_REG);
-
FixupBranch pCTRDontBranch;
if ((inst.BO & BO_DONT_DECREMENT_FLAG) == 0) // Decrement and test CTR
{
@@ -252,18 +214,11 @@ void JitArm64::bcx(UGeckoInstruction inst)
gpr.Flush(FlushMode::MaintainState, WB);
fpr.Flush(FlushMode::MaintainState, ARM64Reg::INVALID_REG);
- if (IsDebuggingEnabled())
+ if (IsBranchWatchEnabled())
{
- ARM64Reg bw_reg_a, bw_reg_b;
- // WC is only allocated when WA is needed for WriteExit and cannot be clobbered.
- if (WC == ARM64Reg::INVALID_REG)
- bw_reg_a = WA, bw_reg_b = WB;
- else
- bw_reg_a = WB, bw_reg_b = WC;
- const BitSet32 gpr_caller_save =
- gpr.GetCallerSavedUsed() & ~BitSet32{DecodeReg(bw_reg_a), DecodeReg(bw_reg_b)};
- WriteBranchWatch<true>(js.compilerPC, js.op->branchTo, inst, bw_reg_a, bw_reg_b,
- gpr_caller_save, fpr.GetCallerSavedUsed());
+ BitSet32 gpr_caller_save = gpr.GetCallerSavedUsed() & ~BitSet32{DecodeReg(WB)};
+ WriteBranchWatch<true>(js.compilerPC, js.op->branchTo, inst, gpr_caller_save,
+ fpr.GetCallerSavedUsed());
}
if (js.op->branchIsIdleLoop)
{
@@ -290,17 +245,15 @@ void JitArm64::bcx(UGeckoInstruction inst)
{
gpr.Flush(FlushMode::All, WA);
fpr.Flush(FlushMode::All, ARM64Reg::INVALID_REG);
- if (IsDebuggingEnabled())
+ if (IsBranchWatchEnabled())
{
- WriteBranchWatch<false>(js.compilerPC, js.compilerPC + 4, inst, WA, WB, {}, {});
+ WriteBranchWatch<false>(js.compilerPC, js.compilerPC + 4, inst, {}, {});
}
WriteExit(js.compilerPC + 4);
}
- else if (IsDebuggingEnabled())
+ else if (IsBranchWatchEnabled())
{
- const BitSet32 gpr_caller_save =
- gpr.GetCallerSavedUsed() & ~BitSet32{DecodeReg(WA), DecodeReg(WB)};
- WriteBranchWatch<false>(js.compilerPC, js.compilerPC + 4, inst, WA, WB, gpr_caller_save,
+ WriteBranchWatch<false>(js.compilerPC, js.compilerPC + 4, inst, gpr.GetCallerSavedUsed(),
fpr.GetCallerSavedUsed());
}
}
@@ -338,14 +291,13 @@ void JitArm64::bcctrx(UGeckoInstruction inst)
LDR(IndexType::Unsigned, WA, PPC_REG, PPCSTATE_OFF_SPR(SPR_CTR));
AND(WA, WA, LogicalImm(~0x3, GPRSize::B32));
- if (IsDebuggingEnabled())
+ if (IsBranchWatchEnabled())
{
- const auto WC = gpr.GetScopedReg(), WD = gpr.GetScopedReg();
BitSet32 gpr_caller_save = BitSet32{DecodeReg(WA)};
if (WB != ARM64Reg::INVALID_REG)
gpr_caller_save[DecodeReg(WB)] = true;
gpr_caller_save &= CALLER_SAVED_GPRS;
- WriteBranchWatchDestInRegister(js.compilerPC, WA, inst, WC, WD, gpr_caller_save, {});
+ WriteBranchWatchDestInRegister(js.compilerPC, WA, inst, gpr_caller_save, {});
}
WriteExit(WA, inst.LK_3, js.compilerPC + 4, WB);
}
@@ -360,18 +312,10 @@ void JitArm64::bclrx(UGeckoInstruction inst)
auto WA = gpr.GetScopedReg();
Arm64GPRCache::ScopedARM64Reg WB;
- if (conditional || inst.LK || IsDebuggingEnabled())
- {
+ if (conditional || inst.LK)
WB = gpr.GetScopedReg();
- }
{
- Arm64GPRCache::ScopedARM64Reg WC;
- if (IsDebuggingEnabled())
- {
- WC = gpr.GetScopedReg();
- }
-
FixupBranch pCTRDontBranch;
if ((inst.BO & BO_DONT_DECREMENT_FLAG) == 0) // Decrement and test CTR
{
@@ -404,13 +348,13 @@ void JitArm64::bclrx(UGeckoInstruction inst)
gpr.Flush(conditional ? FlushMode::MaintainState : FlushMode::All, WB);
fpr.Flush(conditional ? FlushMode::MaintainState : FlushMode::All, ARM64Reg::INVALID_REG);
- if (IsDebuggingEnabled())
+ if (IsBranchWatchEnabled())
{
BitSet32 gpr_caller_save;
BitSet32 fpr_caller_save;
if (conditional)
{
- gpr_caller_save = gpr.GetCallerSavedUsed() & ~BitSet32{DecodeReg(WB), DecodeReg(WC)};
+ gpr_caller_save = gpr.GetCallerSavedUsed();
if (js.op->branchIsIdleLoop)
gpr_caller_save[DecodeReg(WA)] = false;
fpr_caller_save = fpr.GetCallerSavedUsed();
@@ -421,8 +365,7 @@ void JitArm64::bclrx(UGeckoInstruction inst)
js.op->branchIsIdleLoop ? BitSet32{} : BitSet32{DecodeReg(WA)} & CALLER_SAVED_GPRS;
fpr_caller_save = {};
}
- WriteBranchWatchDestInRegister(js.compilerPC, WA, inst, WB, WC, gpr_caller_save,
- fpr_caller_save);
+ WriteBranchWatchDestInRegister(js.compilerPC, WA, inst, gpr_caller_save, fpr_caller_save);
}
if (js.op->branchIsIdleLoop)
{
@@ -449,17 +392,16 @@ void JitArm64::bclrx(UGeckoInstruction inst)
{
gpr.Flush(FlushMode::All, WA);
fpr.Flush(FlushMode::All, ARM64Reg::INVALID_REG);
- if (IsDebuggingEnabled())
+ if (IsBranchWatchEnabled())
{
- WriteBranchWatch<false>(js.compilerPC, js.compilerPC + 4, inst, WA, WB, {}, {});
+ WriteBranchWatch<false>(js.compilerPC, js.compilerPC + 4, inst, {}, {});
}
WriteExit(js.compilerPC + 4);
}
- else if (IsDebuggingEnabled())
+ else if (IsBranchWatchEnabled())
{
- const BitSet32 gpr_caller_save =
- gpr.GetCallerSavedUsed() & ~BitSet32{DecodeReg(WA), DecodeReg(WB)};
- WriteBranchWatch<false>(js.compilerPC, js.compilerPC + 4, inst, WA, WB, gpr_caller_save,
+ const BitSet32 gpr_caller_save = gpr.GetCallerSavedUsed() & ~BitSet32{DecodeReg(WA)};
+ WriteBranchWatch<false>(js.compilerPC, js.compilerPC + 4, inst, gpr_caller_save,
fpr.GetCallerSavedUsed());
}
}
diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_LoadStore.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_LoadStore.cpp
index 2da5d113aa..bcab1ede76 100644
--- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_LoadStore.cpp
+++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_LoadStore.cpp
@@ -826,36 +826,21 @@ void JitArm64::dcbx(UGeckoInstruction inst)
// Load the loop_counter register with the amount of invalidations to execute.
ADD(loop_counter, WA, 1);
- if (IsDebuggingEnabled())
+ if (IsBranchWatchEnabled())
{
- const ARM64Reg branch_watch = EncodeRegTo64(reg_cycle_count);
- MOVP2R(branch_watch, &m_branch_watch);
- LDRB(IndexType::Unsigned, WB, branch_watch, Core::BranchWatch::GetOffsetOfRecordingActive());
- FixupBranch branch_over = CBZ(WB);
-
- FixupBranch branch_in = B();
- SwitchToFarCode();
- SetJumpTarget(branch_in);
-
const BitSet32 gpr_caller_save =
gpr.GetCallerSavedUsed() &
~BitSet32{DecodeReg(WB), DecodeReg(reg_cycle_count), DecodeReg(reg_downcount)};
ABI_PushRegisters(gpr_caller_save);
- const ARM64Reg float_emit_tmp = EncodeRegTo64(WB);
const BitSet32 fpr_caller_save = fpr.GetCallerSavedUsed();
- m_float_emit.ABI_PushRegisters(fpr_caller_save, float_emit_tmp);
+ m_float_emit.ABI_PushRegisters(fpr_caller_save, ARM64Reg::X8);
const PPCAnalyst::CodeOp& op = js.op[2];
ABI_CallFunction(m_ppc_state.msr.IR ? &Core::BranchWatch::HitVirtualTrue_fk_n :
&Core::BranchWatch::HitPhysicalTrue_fk_n,
- branch_watch, Core::FakeBranchWatchCollectionKey{op.address, op.branchTo},
+ &m_branch_watch, Core::FakeBranchWatchCollectionKey{op.address, op.branchTo},
op.inst.hex, WA);
- m_float_emit.ABI_PopRegisters(fpr_caller_save, float_emit_tmp);
+ m_float_emit.ABI_PopRegisters(fpr_caller_save, ARM64Reg::X8);
ABI_PopRegisters(gpr_caller_save);
-
- FixupBranch branch_out = B();
- SwitchToNearCode();
- SetJumpTarget(branch_out);
- SetJumpTarget(branch_over);
}
}
diff --git a/Source/Core/Core/PowerPC/JitCommon/JitBase.h b/Source/Core/Core/PowerPC/JitCommon/JitBase.h
index db615ce7e1..2f366ad07f 100644
--- a/Source/Core/Core/PowerPC/JitCommon/JitBase.h
+++ b/Source/Core/Core/PowerPC/JitCommon/JitBase.h
@@ -23,6 +23,7 @@
#include "Core/PowerPC/JitCommon/JitAsmCommon.h"
#include "Core/PowerPC/JitCommon/JitCache.h"
#include "Core/PowerPC/PPCAnalyst.h"
+#include "Core/PowerPC/PowerPC.h"
namespace Core
{
@@ -200,6 +201,11 @@ public:
bool IsProfilingEnabled() const { return m_enable_profiling && m_enable_debugging; }
bool IsDebuggingEnabled() const { return m_enable_debugging; }
+ bool IsBranchWatchEnabled() const
+ {
+ auto& branch_watch = m_system.GetPowerPC().GetBranchWatch();
+ return branch_watch.GetRecordingActive();
+ }
static const u8* Dispatch(JitBase& jit);
virtual JitBaseBlockCache* GetBlockCache() = 0;
diff --git a/Source/Core/Core/PowerPC/PowerPC.cpp b/Source/Core/Core/PowerPC/PowerPC.cpp
index e5ce8ffcdb..7939b6d4bc 100644
--- a/Source/Core/Core/PowerPC/PowerPC.cpp
+++ b/Source/Core/Core/PowerPC/PowerPC.cpp
@@ -639,7 +639,8 @@ bool PowerPCManager::CheckBreakPoints()
{
const TBreakPoint* bp = m_breakpoints.GetBreakpoint(m_ppc_state.pc);
- if (!bp || !bp->is_enabled || !EvaluateCondition(m_system, bp->condition))
+ if (!m_breakpoints.IsBreakingEnabled() || !bp || !bp->is_enabled ||
+ !EvaluateCondition(m_system, bp->condition))
return false;
if (bp->log_on_hit)
diff --git a/Source/Core/Core/State.cpp b/Source/Core/Core/State.cpp
index fd71ba9df2..98180a8736 100644
--- a/Source/Core/Core/State.cpp
+++ b/Source/Core/Core/State.cpp
@@ -95,7 +95,7 @@ struct CompressAndDumpStateArgs
static Common::WorkQueueThreadSP<CompressAndDumpStateArgs> s_compress_and_dump_thread;
// Don't forget to increase this after doing changes on the savestate system
-constexpr u32 STATE_VERSION = 176; // Last changed in PR 13768
+constexpr u32 STATE_VERSION = 177; // Last changed in PR 13844
// Increase this if the StateExtendedHeader definition changes
constexpr u32 EXTENDED_HEADER_VERSION = 1; // Last changed in PR 12217
diff --git a/Source/Core/Core/TitleDatabase.cpp b/Source/Core/Core/TitleDatabase.cpp
index 523b8b500e..41ad0e0e7e 100644
--- a/Source/Core/Core/TitleDatabase.cpp
+++ b/Source/Core/Core/TitleDatabase.cpp
@@ -91,32 +91,8 @@ TitleDatabase::TitleDatabase()
TitleDatabase::~TitleDatabase() = default;
const std::string& TitleDatabase::GetTitleName(const std::string& gametdb_id,
- const std::string& triforce_id,
DiscIO::Language language) const
{
- if (triforce_id != "")
- {
- const Map& map = *m_triforce_title_maps.at(DiscIO::Language::English);
- auto it = map.find(triforce_id);
- if (it != map.end())
- return it->second;
-
- // This code has been commented out as there is currently only a english title map, and all
- // Triforce games are detected as Japanese.
-
- // if (language != DiscIO::Language::English)
- //{
- // const Map& english_triforce_map = *m_triforce_title_maps.at(DiscIO::Language::English);
- // it = english_triforce_map.find(triforce_id);
- // if (it != english_triforce_map.end())
- // return it->second;
- //}
-
- // it = m_base_map.find(triforce_id);
- // if (it != m_base_map.end())
- // return it->second;
- }
-
auto it = m_user_title_map.find(gametdb_id);
if (it != m_user_title_map.end())
return it->second;
@@ -149,12 +125,12 @@ const std::string& TitleDatabase::GetChannelName(u64 title_id, DiscIO::Language
const std::string id{
{static_cast<char>((title_id >> 24) & 0xff), static_cast<char>((title_id >> 16) & 0xff),
static_cast<char>((title_id >> 8) & 0xff), static_cast<char>(title_id & 0xff)}};
- return GetTitleName(id, "", language);
+ return GetTitleName(id, language);
}
std::string TitleDatabase::Describe(const std::string& gametdb_id, DiscIO::Language language) const
{
- const std::string& title_name = GetTitleName(gametdb_id, "", language);
+ const std::string& title_name = GetTitleName(gametdb_id, language);
if (title_name.empty())
return gametdb_id;
return fmt::format("{} ({})", title_name, gametdb_id);
diff --git a/Source/Core/Core/TitleDatabase.h b/Source/Core/Core/TitleDatabase.h
index dff9a56dfe..d1163d80a6 100644
--- a/Source/Core/Core/TitleDatabase.h
+++ b/Source/Core/Core/TitleDatabase.h
@@ -25,8 +25,7 @@ public:
// Get a user friendly title name for a GameTDB ID.
// This falls back to returning an empty string if none could be found.
- const std::string& GetTitleName(const std::string& gametdb_id, const std::string& triforce_id,
- DiscIO::Language language) const;
+ const std::string& GetTitleName(const std::string& gametdb_id, DiscIO::Language language) const;
// Same as above, but takes a title ID instead of a GameTDB ID, and only works for channels.
const std::string& GetChannelName(u64 title_id, DiscIO::Language language) const;
diff --git a/Source/Core/DiscIO/Blob.h b/Source/Core/DiscIO/Blob.h
index 841a827b97..3551c31d44 100644
--- a/Source/Core/DiscIO/Blob.h
+++ b/Source/Core/DiscIO/Blob.h
@@ -97,6 +97,9 @@ public:
return false;
}
+ // Returns true only for CachedBlobReader.
+ virtual bool IsCached() const { return false; }
+
protected:
BlobReader() {}
};
diff --git a/Source/Core/DiscIO/CachedBlob.cpp b/Source/Core/DiscIO/CachedBlob.cpp
index 98ed8ba31f..f1bbf055ee 100644
--- a/Source/Core/DiscIO/CachedBlob.cpp
+++ b/Source/Core/DiscIO/CachedBlob.cpp
@@ -219,6 +219,8 @@ public:
return m_cache_filler->Read(offset, size, out_ptr) || m_reader->Read(offset, size, out_ptr);
}
+ bool IsCached() const override { return true; }
+
private:
// A shared object does the cache filling for sensible CopyReader behavior.
const std::shared_ptr<CacheFiller> m_cache_filler;
@@ -228,11 +230,17 @@ private:
std::unique_ptr<BlobReader> CreateCachedBlobReader(std::unique_ptr<BlobReader> reader)
{
+ if (reader->IsCached()) // This is already CachedBlobReader.
+ return reader;
+
return std::make_unique<CachedBlobReader>(std::move(reader), false);
}
std::unique_ptr<BlobReader> CreateScrubbingCachedBlobReader(std::unique_ptr<BlobReader> reader)
{
+ if (reader->IsCached()) // This reader is already a CachedBlobReader.
+ return reader;
+
return std::make_unique<CachedBlobReader>(std::move(reader), true);
}
diff --git a/Source/Core/DiscIO/Enums.cpp b/Source/Core/DiscIO/Enums.cpp
index e995479cff..919b10b940 100644
--- a/Source/Core/DiscIO/Enums.cpp
+++ b/Source/Core/DiscIO/Enums.cpp
@@ -407,7 +407,7 @@ std::string GetSysMenuVersionString(u16 title_version, bool is_vwii)
case Region::NTSC_K:
region_letter = 'K';
break;
- case Region::Unknown:
+ default:
WARN_LOG_FMT(DISCIO, "Unknown region for Wii Menu version {}", title_version);
break;
}
diff --git a/Source/Core/DiscIO/Enums.h b/Source/Core/DiscIO/Enums.h
index 9b5e274de9..62ad9e846a 100644
--- a/Source/Core/DiscIO/Enums.h
+++ b/Source/Core/DiscIO/Enums.h
@@ -19,7 +19,7 @@ enum class Platform
WiiDisc,
WiiWAD,
ELFOrDOL,
- NumberOfPlatforms
+ NumberOfPlatforms,
};
enum class Country
@@ -38,7 +38,7 @@ enum class Country
Taiwan,
World,
Unknown,
- NumberOfCountries
+ NumberOfCountries,
};
// This numbering matches Nintendo's GameCube/Wii region numbering.
@@ -48,7 +48,8 @@ enum class Region
NTSC_U = 1, // Mainly North America
PAL = 2, // Mainly Europe and Oceania
Unknown = 3, // Nintendo uses this to mean region free, but we also use it for unknown regions
- NTSC_K = 4 // South Korea (Wii only)
+ NTSC_K = 4, // South Korea (Wii only)
+ DEV = 5, // Triforce has no regions
};
// Languages 0 - 9 match Nintendo's Wii language numbering.
@@ -66,7 +67,7 @@ enum class Language
SimplifiedChinese = 7, // Not selectable on any unmodded retail Wii
TraditionalChinese = 8, // Not selectable on any unmodded retail Wii
Korean = 9,
- Unknown
+ Unknown,
};
std::string GetName(Country country, bool translate);
diff --git a/Source/Core/DiscIO/Volume.h b/Source/Core/DiscIO/Volume.h
index 3f80cd2e3d..32f8e8d71f 100644
--- a/Source/Core/DiscIO/Volume.h
+++ b/Source/Core/DiscIO/Volume.h
@@ -8,6 +8,7 @@
#include <map>
#include <memory>
#include <optional>
+#include <span>
#include <string>
#include <vector>
@@ -96,7 +97,6 @@ public:
}
virtual std::string GetGameID(const Partition& partition = PARTITION_NONE) const = 0;
virtual std::string GetGameTDBID(const Partition& partition = PARTITION_NONE) const = 0;
- virtual std::string GetTriforceID() const { return ""; }
virtual std::string GetMakerID(const Partition& partition = PARTITION_NONE) const = 0;
virtual std::optional<u16> GetRevision(const Partition& partition = PARTITION_NONE) const = 0;
virtual std::string GetInternalName(const Partition& partition = PARTITION_NONE) const = 0;
@@ -144,16 +144,15 @@ public:
virtual std::array<u8, 20> GetSyncHash() const = 0;
protected:
- template <u32 N>
- std::string DecodeString(const char (&data)[N]) const
+ std::string DecodeString(std::span<const char> data) const
{
// strnlen to trim NULLs
- std::string string(data, strnlen(data, sizeof(data)));
+ std::string string(data.data(), strnlen(data.data(), data.size()));
if (GetRegion() == Region::NTSC_J)
return SHIFTJISToUTF8(string);
- else
- return CP1252ToUTF8(string);
+
+ return CP1252ToUTF8(string);
}
void ReadAndAddToSyncHash(Common::SHA1::Context* context, u64 offset, u64 length,
diff --git a/Source/Core/DiscIO/VolumeDisc.cpp b/Source/Core/DiscIO/VolumeDisc.cpp
index 882bd3ac96..ecbb4a9dc9 100644
--- a/Source/Core/DiscIO/VolumeDisc.cpp
+++ b/Source/Core/DiscIO/VolumeDisc.cpp
@@ -12,12 +12,46 @@
#include "DiscIO/DiscUtils.h"
#include "DiscIO/Enums.h"
#include "DiscIO/Filesystem.h"
+#include "DiscIO/VolumeGC.h"
namespace DiscIO
{
std::string VolumeDisc::GetGameID(const Partition& partition) const
{
- char id[6];
+ char id[6]{};
+
+ if (GetVolumeType() == Platform::Triforce)
+ {
+ // Triforce games have their Game ID stored in the boot.id file
+ const BootID* boot_id = static_cast<const VolumeGC*>(this)->GetTriforceBootID();
+
+ // Construct game ID from the BTID
+ id[0] = 'G';
+
+ memcpy(id + 1, boot_id->game_id.data() + 2, 2);
+
+ switch (GetCountry())
+ {
+ default:
+ case Country::Japan:
+ id[3] = 'J';
+ break;
+ case Country::Taiwan:
+ id[3] = 'W';
+ break;
+ case Country::USA:
+ id[3] = 'E';
+ break;
+ case Country::Europe:
+ id[3] = 'P';
+ break;
+ }
+
+ const std::string maker_id{GetMakerID()};
+ memcpy(id + 4, maker_id.c_str(), std::min<std::size_t>(maker_id.size(), 2));
+
+ return DecodeString(id);
+ }
if (!Read(0, sizeof(id), reinterpret_cast<u8*>(id), partition))
return std::string();
@@ -27,8 +61,35 @@ std::string VolumeDisc::GetGameID(const Partition& partition) const
Country VolumeDisc::GetCountry(const Partition& partition) const
{
- // The 0 that we use as a default value is mapped to Country::Unknown and Region::Unknown
- const u8 country_byte = ReadSwapped<u8>(3, partition).value_or(0);
+ u8 country_byte = 0;
+
+ if (GetVolumeType() == Platform::Triforce)
+ {
+ const BootID* boot_id = static_cast<const VolumeGC*>(this)->GetTriforceBootID();
+
+ switch (boot_id->region_flags)
+ {
+ default:
+ case 0x02: // JAPAN
+ country_byte = 'J';
+ break;
+ case 0x08: // ASIA
+ country_byte = 'W';
+ break;
+ case 0x0E: // USA
+ country_byte = 'E';
+ break;
+ case 0x0C: // EXPORT
+ country_byte = 'P';
+ break;
+ }
+ }
+ else
+ {
+ // The 0 that we use as a default value is mapped to Country::Unknown and Region::Unknown
+ country_byte = ReadSwapped<u8>(3, partition).value_or(0);
+ }
+
const Region region = GetRegion();
const std::optional<u16> revision = GetRevision();
@@ -51,6 +112,29 @@ std::string VolumeDisc::GetMakerID(const Partition& partition) const
{
char maker_id[2];
+ // Triforce games have their maker stored in the boot.id file as an actual string
+ if (GetVolumeType() == Platform::Triforce)
+ {
+ const BootID* boot_id = static_cast<const VolumeGC*>(this)->GetTriforceBootID();
+
+ // There only seem to be three different makers here,
+ // so we can just check the first char to difference between them.
+ //
+ // NAMCO CORPORATION, SEGA CORPORATION and Hitmaker co,ltd.
+
+ switch (boot_id->manufacturer[0])
+ {
+ case 'S': // SEGA CORPORATION
+ case 'H': // Hitmaker co,ltd
+ return DecodeString("6E");
+ case 'N': // NAMCO CORPORATION
+ return DecodeString("82");
+ default:
+ break;
+ }
+ // Fall back to normal maker from header
+ }
+
if (!Read(0x4, sizeof(maker_id), reinterpret_cast<u8*>(&maker_id), partition))
return std::string();
@@ -66,6 +150,14 @@ std::optional<u16> VolumeDisc::GetRevision(const Partition& partition) const
std::string VolumeDisc::GetInternalName(const Partition& partition) const
{
char name[0x60];
+
+ // Triforce games have their Title stored in the boot.id file
+ if (GetVolumeType() == Platform::Triforce)
+ {
+ const BootID* boot_id = static_cast<const VolumeGC*>(this)->GetTriforceBootID();
+ return DecodeString(boot_id->game_name);
+ }
+
if (!Read(0x20, sizeof(name), reinterpret_cast<u8*>(&name), partition))
return std::string();
diff --git a/Source/Core/DiscIO/VolumeDisc.h b/Source/Core/DiscIO/VolumeDisc.h
index 17c76ae689..f630522e68 100644
--- a/Source/Core/DiscIO/VolumeDisc.h
+++ b/Source/Core/DiscIO/VolumeDisc.h
@@ -3,6 +3,7 @@
#pragma once
+#include <array>
#include <optional>
#include <string>
@@ -15,6 +16,30 @@ namespace DiscIO
class VolumeDisc : public Volume
{
public:
+ struct BootID
+ {
+ u32 magic; // 0x00 (Always BTID)
+ std::array<uint32_t, 3> unknown0;
+ std::array<uint32_t, 4> unknown1;
+ std::array<char, 4> mediaboard_type; // 0x20 (GCAM for Triforce)
+ uint32_t unknown2;
+ uint16_t year; // 0x28
+ uint8_t month; // 0x2A
+ uint8_t day; // 0x2B
+ uint8_t video_mode; // 0x2C unknown bitmask, resolutions + horizontal/vertical
+ uint8_t unknown3;
+ uint8_t type_3_compatible; // 0x2E (Type-3 compatible titles have this set to 1)
+ uint8_t unknown4;
+ std::array<char, 8> game_id; // 0x30
+ uint32_t region_flags; // 0x38
+ std::array<uint32_t, 9> unknown6;
+ std::array<char, 0x20> manufacturer; // 0x60
+ std::array<char, 0x20> game_name; // 0x80
+ std::array<char, 0x20> game_executable; // 0xA0
+ std::array<char, 0x20> test_executable; // 0xC0
+ std::array<std::array<char, 0x20>, 8> credit_types; // 0xE0
+ };
+
std::string GetGameID(const Partition& partition = PARTITION_NONE) const override;
Country GetCountry(const Partition& partition = PARTITION_NONE) const override;
std::string GetMakerID(const Partition& partition = PARTITION_NONE) const override;
@@ -29,4 +54,6 @@ protected:
void AddGamePartitionToSyncHash(Common::SHA1::Context* context) const;
};
+static_assert(sizeof(VolumeDisc::BootID) == 480);
+
} // namespace DiscIO
diff --git a/Source/Core/DiscIO/VolumeGC.cpp b/Source/Core/DiscIO/VolumeGC.cpp
index ad3d8ec74e..5d3d372dcf 100644
--- a/Source/Core/DiscIO/VolumeGC.cpp
+++ b/Source/Core/DiscIO/VolumeGC.cpp
@@ -45,13 +45,11 @@ VolumeGC::VolumeGC(std::unique_ptr<BlobReader> reader)
std::unique_ptr<FileInfo> file_info = tmp_fs->FindFileInfo("boot.id");
if (!file_info)
return;
- BootID triforce_header;
const u64 file_size = ReadFile(*this, PARTITION_NONE, file_info.get(),
- reinterpret_cast<u8*>(&triforce_header), sizeof(BootID));
- if (file_size >= 4 && triforce_header.magic == BTID_MAGIC)
+ reinterpret_cast<u8*>(&m_triforce_header), sizeof(BootID));
+ if (file_size >= 4 && m_triforce_header.magic == BTID_MAGIC)
{
m_is_triforce = true;
- m_triforce_id = triforce_header.id;
}
}
}
@@ -90,16 +88,23 @@ std::string VolumeGC::GetGameTDBID(const Partition& partition) const
return GetGameID(partition);
}
-std::string VolumeGC::GetTriforceID() const
+Region VolumeGC::GetRegion() const
{
if (m_is_triforce)
- return (std::string(m_triforce_id.data(), m_triforce_id.size()));
- else
- return "";
-}
+ {
+ switch (m_triforce_header.region_flags)
+ {
+ default:
+ case 0x02: // JAPAN
+ case 0x08: // ASIA
+ return Region::NTSC_J;
+ case 0x0E: // USA
+ return Region::NTSC_U;
+ case 0x0C: // EXPORT
+ return Region::PAL;
+ }
+ }
-Region VolumeGC::GetRegion() const
-{
return RegionCodeToRegion(m_reader->ReadSwapped<u32>(0x458));
}
@@ -184,6 +189,12 @@ std::array<u8, 20> VolumeGC::GetSyncHash() const
VolumeGC::ConvertedGCBanner VolumeGC::LoadBannerFile() const
{
+ // There is at least one Triforce game that has an opening.bnr file but from a different game.
+ if (m_is_triforce)
+ {
+ return {};
+ }
+
GCBanner banner_file;
const u64 file_size = ReadFile(*this, PARTITION_NONE, "opening.bnr",
reinterpret_cast<u8*>(&banner_file), sizeof(GCBanner));
diff --git a/Source/Core/DiscIO/VolumeGC.h b/Source/Core/DiscIO/VolumeGC.h
index ff768c2e2a..2f85a75535 100644
--- a/Source/Core/DiscIO/VolumeGC.h
+++ b/Source/Core/DiscIO/VolumeGC.h
@@ -34,7 +34,6 @@ public:
const Partition& partition = PARTITION_NONE) const override;
const FileSystem* GetFileSystem(const Partition& partition = PARTITION_NONE) const override;
std::string GetGameTDBID(const Partition& partition = PARTITION_NONE) const override;
- std::string GetTriforceID() const override;
std::map<Language, std::string> GetShortNames() const override;
std::map<Language, std::string> GetLongNames() const override;
std::map<Language, std::string> GetShortMakers() const override;
@@ -50,9 +49,10 @@ public:
DataSizeType GetDataSizeType() const override;
u64 GetRawSize() const override;
const BlobReader& GetBlobReader() const override;
-
std::array<u8, 20> GetSyncHash() const override;
+ const BootID* GetTriforceBootID() const { return &m_triforce_header; }
+
private:
static constexpr u32 GC_BANNER_WIDTH = 96;
static constexpr u32 GC_BANNER_HEIGHT = 32;
@@ -77,13 +77,6 @@ private:
// (only one for BNR1 type)
};
- struct BootID
- {
- u32 magic; // "BTID"
- u32 padding[11];
- std::array<char, 4> id;
- };
-
struct ConvertedGCBanner
{
ConvertedGCBanner();
@@ -112,8 +105,9 @@ private:
std::unique_ptr<BlobReader> m_reader;
+ BootID m_triforce_header;
+
bool m_is_triforce;
- std::array<char, 4> m_triforce_id;
};
} // namespace DiscIO
diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props
index d030f8821b..34e4b83f13 100644
--- a/Source/Core/DolphinLib.props
+++ b/Source/Core/DolphinLib.props
@@ -279,6 +279,7 @@
<ClInclude Include="Core\HW\DSPLLE\DSPDebugInterface.h" />
<ClInclude Include="Core\HW\DSPLLE\DSPLLE.h" />
<ClInclude Include="Core\HW\DSPLLE\DSPSymbols.h" />
+ <ClInclude Include="Core\HW\DVD\AMMediaboard.h" />
<ClInclude Include="Core\HW\DVD\DVDInterface.h" />
<ClInclude Include="Core\HW\DVD\DVDMath.h" />
<ClInclude Include="Core\HW\DVD\DVDThread.h" />
@@ -316,12 +317,16 @@
<ClInclude Include="Core\HW\HSP\HSP_DeviceARAMExpansion.h" />
<ClInclude Include="Core\HW\HSP\HSP_DeviceNull.h" />
<ClInclude Include="Core\HW\HW.h" />
+ <ClInclude Include="Core\HW\MagCard\C1231BR.h" />
+ <ClInclude Include="Core\HW\MagCard\C1231LR.h" />
+ <ClInclude Include="Core\HW\MagCard\MagneticCardReader.h" />
<ClInclude Include="Core\HW\Memmap.h" />
<ClInclude Include="Core\HW\MemoryInterface.h" />
<ClInclude Include="Core\HW\MMIO.h" />
<ClInclude Include="Core\HW\MMIOHandlers.h" />
<ClInclude Include="Core\HW\ProcessorInterface.h" />
<ClInclude Include="Core\HW\SI\SI_Device.h" />
+ <ClInclude Include="Core\HW\SI\SI_DeviceAMBaseboard.h" />
<ClInclude Include="Core\HW\SI\SI_DeviceDanceMat.h" />
<ClInclude Include="Core\HW\SI\SI_DeviceGBA.h" />
<ClInclude Include="Core\HW\SI\SI_DeviceGBAEmu.h" />
@@ -971,6 +976,7 @@
<ClCompile Include="Core\HW\DSPLLE\DSPHost.cpp" />
<ClCompile Include="Core\HW\DSPLLE\DSPLLE.cpp" />
<ClCompile Include="Core\HW\DSPLLE\DSPSymbols.cpp" />
+ <ClCompile Include="Core\HW\DVD\AMMediaboard.cpp" />
<ClCompile Include="Core\HW\DVD\DVDInterface.cpp" />
<ClCompile Include="Core\HW\DVD\DVDMath.cpp" />
<ClCompile Include="Core\HW\DVD\DVDThread.cpp" />
@@ -1012,11 +1018,15 @@
<ClCompile Include="Core\HW\HSP\HSP_DeviceARAMExpansion.cpp" />
<ClCompile Include="Core\HW\HSP\HSP_DeviceNull.cpp" />
<ClCompile Include="Core\HW\HW.cpp" />
+ <ClCompile Include="Core\HW\MagCard\C1231BR.cpp" />
+ <ClCompile Include="Core\HW\MagCard\C1231LR.cpp" />
+ <ClCompile Include="Core\HW\MagCard\MagneticCardReader.cpp" />
<ClCompile Include="Core\HW\Memmap.cpp" />
<ClCompile Include="Core\HW\MemoryInterface.cpp" />
<ClCompile Include="Core\HW\MMIO.cpp" />
<ClCompile Include="Core\HW\ProcessorInterface.cpp" />
<ClCompile Include="Core\HW\SI\SI_Device.cpp" />
+ <ClCompile Include="Core\HW\SI\SI_DeviceAMBaseboard.cpp" />
<ClCompile Include="Core\HW\SI\SI_DeviceDanceMat.cpp" />
<ClCompile Include="Core\HW\SI\SI_DeviceGBA.cpp" />
<ClCompile Include="Core\HW\SI\SI_DeviceGBAEmu.cpp" />
diff --git a/Source/Core/DolphinQt/CMakeLists.txt b/Source/Core/DolphinQt/CMakeLists.txt
index 622fb35e1f..e7e70c508b 100644
--- a/Source/Core/DolphinQt/CMakeLists.txt
+++ b/Source/Core/DolphinQt/CMakeLists.txt
@@ -378,6 +378,8 @@ add_executable(dolphin-emu
Settings/OnScreenDisplayPane.h
Settings/PathPane.cpp
Settings/PathPane.h
+ Settings/TriforcePane.cpp
+ Settings/TriforcePane.h
Settings/USBDevicePicker.cpp
Settings/USBDevicePicker.h
Settings/WiiPane.cpp
diff --git a/Source/Core/DolphinQt/CheatSearchWidget.cpp b/Source/Core/DolphinQt/CheatSearchWidget.cpp
index 629d8dbafb..35468d5fcc 100644
--- a/Source/Core/DolphinQt/CheatSearchWidget.cpp
+++ b/Source/Core/DolphinQt/CheatSearchWidget.cpp
@@ -3,7 +3,9 @@
#include "DolphinQt/CheatSearchWidget.h"
+#include <algorithm>
#include <optional>
+#include <ranges>
#include <string>
#include <unordered_map>
#include <utility>
@@ -224,6 +226,7 @@ void CheatSearchWidget::CreateWidgets()
m_address_table = new QTableWidget();
m_address_table->setContextMenuPolicy(Qt::CustomContextMenu);
+ m_address_table->setSelectionBehavior(QAbstractItemView::SelectRows);
m_info_label_1 = new QLabel(tr("Waiting for first scan..."));
m_info_label_2 = new QLabel();
@@ -483,6 +486,8 @@ void CheatSearchWidget::OnAddressTableContextMenu()
if (m_address_table->selectedItems().isEmpty())
return;
+ std::vector<const QTableWidgetItem*> selected_items = GetSelectedAddressTableItems();
+
QMenu* menu = new QMenu(this);
menu->setAttribute(Qt::WA_DeleteOnClose, true);
@@ -491,8 +496,8 @@ void CheatSearchWidget::OnAddressTableContextMenu()
const u32 address = item->data(ADDRESS_TABLE_ADDRESS_ROLE).toUInt();
emit ShowMemory(address);
});
- menu->addAction(tr("Add to watch"), this, [this] {
- for (auto* const item : m_address_table->selectedItems())
+ menu->addAction(tr("Add to watch"), this, [this, selected_items] {
+ for (auto* const item : selected_items)
{
const u32 address = item->data(ADDRESS_TABLE_ADDRESS_ROLE).toUInt();
const QString name = QStringLiteral("mem_%1").arg(address, 8, 16, QLatin1Char('0'));
@@ -501,6 +506,15 @@ void CheatSearchWidget::OnAddressTableContextMenu()
});
menu->addAction(tr("Generate Action Replay Code(s)"), this, &CheatSearchWidget::GenerateARCodes);
menu->addAction(tr("Write value"), this, &CheatSearchWidget::WriteValue);
+ menu->addAction(tr("Delete Address"), this, [this, selected_items] {
+ // Process in reverse so removal won't change the index of items about to be processed.
+ for (auto* const item : selected_items | std::views::reverse)
+ {
+ const u32 index = item->data(ADDRESS_TABLE_RESULT_INDEX_ROLE).toUInt();
+ m_last_value_session->RemoveResult(index);
+ }
+ RecreateGUITable();
+ });
menu->exec(QCursor::pos());
}
@@ -533,7 +547,7 @@ void CheatSearchWidget::GenerateARCodes()
bool had_multiple_errors = false;
std::optional<Cheats::GenerateActionReplayCodeErrorCode> error_code;
- for (auto* const item : m_address_table->selectedItems())
+ for (auto* const item : GetSelectedAddressTableItems())
{
const u32 index = item->data(ADDRESS_TABLE_RESULT_INDEX_ROLE).toUInt();
const auto result = Cheats::GenerateActionReplayCode(*m_last_value_session, index);
@@ -600,9 +614,9 @@ void CheatSearchWidget::WriteValue()
return;
}
- auto items = m_address_table->selectedItems();
+ auto items = GetSelectedAddressTableItems();
std::vector<u32> addresses(items.size());
- std::transform(items.begin(), items.end(), addresses.begin(), [](QTableWidgetItem* item) {
+ std::transform(items.begin(), items.end(), addresses.begin(), [](const QTableWidgetItem* item) {
return item->data(ADDRESS_TABLE_ADDRESS_ROLE).toUInt();
});
Core::CPUThreadGuard guard{m_system};
@@ -610,6 +624,7 @@ void CheatSearchWidget::WriteValue()
{
m_info_label_1->setText(tr("There was an error writing (some) values."));
}
+ UpdateTableAllCurrentValues(UpdateSource::User);
}
size_t CheatSearchWidget::GetTableRowCount() const
@@ -638,6 +653,26 @@ void CheatSearchWidget::RefreshGUICurrentValues(const size_t begin_index, const
}
}
+const std::vector<const QTableWidgetItem*> CheatSearchWidget::GetSelectedAddressTableItems() const
+{
+ // Don't process each selectedItems(), as it can produce duplicate commands for one address when
+ // multiple items in the same row are selected. Instead, uses rows and gets one item from each
+ // row. All row items have identical data.
+ auto selected_rows = m_address_table->selectionModel()->selectedRows();
+
+ // Ascending address order.
+ std::sort(selected_rows.begin(), selected_rows.end(),
+ [](const QModelIndex& a, const QModelIndex& b) { return a.row() < b.row(); });
+
+ std::vector<const QTableWidgetItem*> selected_items;
+ for (const auto& index : selected_rows)
+ {
+ const int row = index.row();
+ selected_items.push_back(m_address_table->item(row, 0));
+ }
+ return selected_items;
+}
+
void CheatSearchWidget::RecreateGUITable()
{
const QSignalBlocker blocker(m_address_table);
diff --git a/Source/Core/DolphinQt/CheatSearchWidget.h b/Source/Core/DolphinQt/CheatSearchWidget.h
index 18aa5a14b0..5dec20c3ed 100644
--- a/Source/Core/DolphinQt/CheatSearchWidget.h
+++ b/Source/Core/DolphinQt/CheatSearchWidget.h
@@ -75,6 +75,7 @@ private:
int GetVisibleRowsBeginIndex() const;
int GetVisibleRowsEndIndex() const;
size_t GetTableRowCount() const;
+ const std::vector<const QTableWidgetItem*> GetSelectedAddressTableItems() const;
Core::System& m_system;
diff --git a/Source/Core/DolphinQt/Config/GamecubeControllersWidget.cpp b/Source/Core/DolphinQt/Config/GamecubeControllersWidget.cpp
index d23db46eb6..33c54edbd6 100644
--- a/Source/Core/DolphinQt/Config/GamecubeControllersWidget.cpp
+++ b/Source/Core/DolphinQt/Config/GamecubeControllersWidget.cpp
@@ -42,6 +42,7 @@ static constexpr std::array s_gc_types = {
#endif
SIDeviceName{SerialInterface::SIDEVICE_GC_GBA, _trans("GBA (TCP)")},
SIDeviceName{SerialInterface::SIDEVICE_GC_KEYBOARD, _trans("Keyboard Controller")},
+ SIDeviceName{SerialInterface::SIDEVICE_AM_BASEBOARD, _trans("Triforce Baseboard")},
};
static std::optional<int> ToGCMenuIndex(const SerialInterface::SIDevices sidevice)
@@ -156,6 +157,9 @@ void GamecubeControllersWidget::OnGCPadConfigure(size_t index)
case SerialInterface::SIDEVICE_GC_KEYBOARD:
type = MappingWindow::Type::MAPPING_GC_KEYBOARD;
break;
+ case SerialInterface::SIDEVICE_AM_BASEBOARD:
+ type = MappingWindow::Type::MAPPING_AM_BASEBOARD;
+ break;
default:
return;
}
diff --git a/Source/Core/DolphinQt/Config/InfoWidget.cpp b/Source/Core/DolphinQt/Config/InfoWidget.cpp
index fb1e3d0112..4ee3f63bd6 100644
--- a/Source/Core/DolphinQt/Config/InfoWidget.cpp
+++ b/Source/Core/DolphinQt/Config/InfoWidget.cpp
@@ -122,12 +122,6 @@ QGroupBox* InfoWidget::CreateGameDetails()
m_game.GetMakerID() + ")");
layout->addRow(tr("Name:"), internal_name);
- if (m_game.GetPlatform() == DiscIO::Platform::Triforce)
- {
- const auto triforce_id_string = QString::fromStdString(m_game.GetTriforceID());
- auto* const triforce_id = CreateValueDisplay(triforce_id_string);
- layout->addRow(tr("Triforce ID:"), triforce_id);
- }
layout->addRow(tr("Game ID:"), game_id);
layout->addRow(tr("Country:"), country);
layout->addRow(tr("Maker:"), maker);
diff --git a/Source/Core/DolphinQt/Config/Mapping/GCPadEmu.cpp b/Source/Core/DolphinQt/Config/Mapping/GCPadEmu.cpp
index e530f495a5..a8af073792 100644
--- a/Source/Core/DolphinQt/Config/Mapping/GCPadEmu.cpp
+++ b/Source/Core/DolphinQt/Config/Mapping/GCPadEmu.cpp
@@ -11,19 +11,25 @@
#include "InputCommon/InputConfig.h"
-GCPadEmu::GCPadEmu(MappingWindow* window) : MappingWidget(window)
+GCPadEmu::GCPadEmu(MappingWindow* window, SubType sub_type) : MappingWidget(window)
{
- CreateMainLayout();
+ CreateMainLayout(sub_type);
}
-void GCPadEmu::CreateMainLayout()
+void GCPadEmu::CreateMainLayout(SubType sub_type)
{
auto* layout = new QGridLayout;
layout->addWidget(CreateGroupBox(tr("Buttons"), Pad::GetGroup(GetPort(), PadGroup::Buttons)), 0,
0);
- layout->addWidget(CreateGroupBox(tr("D-Pad"), Pad::GetGroup(GetPort(), PadGroup::DPad)), 1, 0, -1,
- 1);
+ layout->addWidget(CreateGroupBox(tr("D-Pad"), Pad::GetGroup(GetPort(), PadGroup::DPad)), 1, 0);
+
+ if (sub_type == SubType::AMBaseboard)
+ {
+ layout->addWidget(CreateGroupBox(tr("Triforce"), Pad::GetGroup(GetPort(), PadGroup::Triforce)),
+ 2, 0);
+ }
+
layout->addWidget(
CreateGroupBox(tr("Control Stick"), Pad::GetGroup(GetPort(), PadGroup::MainStick)), 0, 1, -1,
1);
diff --git a/Source/Core/DolphinQt/Config/Mapping/GCPadEmu.h b/Source/Core/DolphinQt/Config/Mapping/GCPadEmu.h
index dad55b4c76..6b315b2a2c 100644
--- a/Source/Core/DolphinQt/Config/Mapping/GCPadEmu.h
+++ b/Source/Core/DolphinQt/Config/Mapping/GCPadEmu.h
@@ -9,12 +9,28 @@ class GCPadEmu final : public MappingWidget
{
Q_OBJECT
public:
- explicit GCPadEmu(MappingWindow* window);
+ enum class SubType
+ {
+ StandardController,
+ AMBaseboard,
+ };
+
+ GCPadEmu(MappingWindow* window, SubType sub_type);
InputConfig* GetConfig() override;
private:
void LoadSettings() override;
void SaveSettings() override;
- void CreateMainLayout();
+ void CreateMainLayout(SubType sub_type);
};
+
+[[nodiscard]] inline auto CreateStandardControllerMappingWidget(MappingWindow* parent)
+{
+ return new GCPadEmu{parent, GCPadEmu::SubType::StandardController};
+}
+
+[[nodiscard]] inline auto CreateAMBaseboardMappingWidget(MappingWindow* parent)
+{
+ return new GCPadEmu{parent, GCPadEmu::SubType::AMBaseboard};
+}
diff --git a/Source/Core/DolphinQt/Config/Mapping/MappingWindow.cpp b/Source/Core/DolphinQt/Config/Mapping/MappingWindow.cpp
index ddb9d39ee6..d52e950a27 100644
--- a/Source/Core/DolphinQt/Config/Mapping/MappingWindow.cpp
+++ b/Source/Core/DolphinQt/Config/Mapping/MappingWindow.cpp
@@ -26,6 +26,9 @@
#include "Common/IniFile.h"
#include "Common/StringUtil.h"
+#include "Core/HW/SI/SI.h"
+#include "Core/HW/SI/SI_DeviceAMBaseboard.h"
+
#include "DolphinQt/Config/Mapping/FreeLookGeneral.h"
#include "DolphinQt/Config/Mapping/FreeLookRotation.h"
#include "DolphinQt/Config/Mapping/GBAPadEmu.h"
@@ -457,7 +460,7 @@ void MappingWindow::SetMappingType(MappingWindow::Type type)
case Type::MAPPING_GC_STEERINGWHEEL:
case Type::MAPPING_GC_DANCEMAT:
case Type::MAPPING_GCPAD:
- widget = new GCPadEmu(this);
+ widget = CreateStandardControllerMappingWidget(this);
setWindowTitle(tr("GameCube Controller at Port %1").arg(GetPort() + 1));
AddWidget(tr("GameCube Controller"), widget);
break;
@@ -516,6 +519,11 @@ void MappingWindow::SetMappingType(MappingWindow::Type type)
setWindowTitle(tr("Free Look Controller %1").arg(GetPort() + 1));
}
break;
+ case Type::MAPPING_AM_BASEBOARD:
+ widget = CreateAMBaseboardMappingWidget(this);
+ setWindowTitle(tr("Triforce Baseboard at Port %1").arg(GetPort() + 1));
+ AddWidget(tr("Triforce Baseboard"), widget);
+ break;
default:
return;
}
diff --git a/Source/Core/DolphinQt/Config/Mapping/MappingWindow.h b/Source/Core/DolphinQt/Config/Mapping/MappingWindow.h
index 222d3e3a58..40789b8469 100644
--- a/Source/Core/DolphinQt/Config/Mapping/MappingWindow.h
+++ b/Source/Core/DolphinQt/Config/Mapping/MappingWindow.h
@@ -45,6 +45,8 @@ public:
MAPPING_HOTKEYS,
// Freelook
MAPPING_FREELOOK,
+ // Triforce
+ MAPPING_AM_BASEBOARD,
};
explicit MappingWindow(QWidget* parent, Type type, int port_num);
diff --git a/Source/Core/DolphinQt/Config/SettingsWindow.cpp b/Source/Core/DolphinQt/Config/SettingsWindow.cpp
index 2898b4cd8a..e80b1fa7ff 100644
--- a/Source/Core/DolphinQt/Config/SettingsWindow.cpp
+++ b/Source/Core/DolphinQt/Config/SettingsWindow.cpp
@@ -29,6 +29,7 @@
#include "DolphinQt/Settings/InterfacePane.h"
#include "DolphinQt/Settings/OnScreenDisplayPane.h"
#include "DolphinQt/Settings/PathPane.h"
+#include "DolphinQt/Settings/TriforcePane.h"
#include "DolphinQt/Settings/WiiPane.h"
StackedSettingsWindow::StackedSettingsWindow(QWidget* parent) : QDialog{parent}
@@ -197,8 +198,9 @@ SettingsWindow::SettingsWindow(MainWindow* parent) : StackedSettingsWindow{paren
AddWrappedPane(new OnScreenDisplayPane, tr("On-Screen Display"));
AddWrappedPane(new AudioPane, tr("Audio"));
AddWrappedPane(new PathPane, tr("Paths"));
- AddWrappedPane(new GameCubePane, tr("GameCube"));
+ AddWrappedPane(new GameCubePane{parent}, tr("GameCube"));
AddWrappedPane(new WiiPane, tr("Wii"));
+ AddWrappedPane(new TriforcePane, tr("Triforce"));
AddWrappedPane(new AdvancedPane, tr("Advanced"));
OnDoneCreatingPanes();
diff --git a/Source/Core/DolphinQt/Config/SettingsWindow.h b/Source/Core/DolphinQt/Config/SettingsWindow.h
index 9048e83385..3874c75ae0 100644
--- a/Source/Core/DolphinQt/Config/SettingsWindow.h
+++ b/Source/Core/DolphinQt/Config/SettingsWindow.h
@@ -49,6 +49,7 @@ enum class SettingsWindowPaneIndex : int
Paths,
GameCube,
Wii,
+ Triforce,
Advanced,
};
diff --git a/Source/Core/DolphinQt/Debugger/BreakpointWidget.cpp b/Source/Core/DolphinQt/Debugger/BreakpointWidget.cpp
index 09b045688c..fbb5c38d9b 100644
--- a/Source/Core/DolphinQt/Debugger/BreakpointWidget.cpp
+++ b/Source/Core/DolphinQt/Debugger/BreakpointWidget.cpp
@@ -173,6 +173,7 @@ void BreakpointWidget::CreateWidgets()
layout->setContentsMargins(2, 2, 2, 2);
layout->setSpacing(0);
+ m_enabled = m_toolbar->addAction(tr("Disable"), this, &BreakpointWidget::OnToggleBreaking);
m_new = m_toolbar->addAction(tr("New"), this, &BreakpointWidget::OnNewBreakpoint);
m_clear = m_toolbar->addAction(tr("Clear"), this, &BreakpointWidget::OnClear);
@@ -190,6 +191,10 @@ void BreakpointWidget::CreateWidgets()
void BreakpointWidget::UpdateIcons()
{
+ if (m_system.GetPowerPC().GetBreakPoints().IsBreakingEnabled())
+ m_enabled->setIcon(Resources::GetThemeIcon("pause"));
+ else
+ m_enabled->setIcon(Resources::GetThemeIcon("play"));
m_new->setIcon(Resources::GetThemeIcon("debugger_add_breakpoint"));
m_clear->setIcon(Resources::GetThemeIcon("debugger_clear"));
m_load->setIcon(Resources::GetThemeIcon("debugger_load"));
@@ -289,6 +294,24 @@ void BreakpointWidget::Update()
QPixmap enabled_icon =
Resources::GetThemeIcon("debugger_breakpoint").pixmap(QSize(downscale, downscale));
+ auto& power_pc = m_system.GetPowerPC();
+ auto& breakpoints = power_pc.GetBreakPoints();
+
+ if (!breakpoints.IsBreakingEnabled())
+ {
+ // Use QPainter to draw a transparent hole in the center
+ QImage image = enabled_icon.toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied);
+ QPainter painter(&image);
+ painter.setCompositionMode(QPainter::CompositionMode_Source);
+ painter.setRenderHint(QPainter::Antialiasing, true);
+ painter.setPen(Qt::NoPen);
+ painter.setBrush(Qt::transparent);
+ // Center and radius
+ painter.drawEllipse(QPoint(downscale / 2, downscale / 2), downscale / 4, downscale / 4);
+ painter.end();
+ enabled_icon = QPixmap::fromImage(image);
+ }
+
const auto create_item = [](const QString& string = {}) {
QTableWidgetItem* item = new QTableWidgetItem(string);
item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
@@ -306,8 +329,6 @@ void BreakpointWidget::Update()
Settings::Instance().IsThemeDark() ? QColor(75, 75, 75) : QColor(225, 225, 225);
disabled_item.setBackground(disabled_color);
- auto& power_pc = m_system.GetPowerPC();
- auto& breakpoints = power_pc.GetBreakPoints();
auto& memchecks = power_pc.GetMemChecks();
auto& ppc_symbol_db = power_pc.GetSymbolDB();
@@ -466,6 +487,36 @@ void BreakpointWidget::OnEditBreakpoint(u32 address, bool is_instruction_bp)
emit Host::GetInstance()->PPCBreakpointsChanged();
}
+void BreakpointWidget::OnToggleBreaking()
+{
+ auto& breakpoints = m_system.GetPowerPC().GetBreakPoints();
+ auto& memchecks = m_system.GetPowerPC().GetMemChecks();
+ // Memcheck's HasAny() will report no memchecks while breaking is disabled, so only check when
+ // breaking is true.
+ bool has_memory_bp;
+
+ // Currently toggles all code and memory breakpoints. Could be split if needed.
+ if (breakpoints.IsBreakingEnabled())
+ {
+ has_memory_bp = memchecks.HasAny();
+ breakpoints.EnableBreaking(false);
+ memchecks.EnableBreaking(false);
+ m_enabled->setText(tr("Enable"));
+ m_enabled->setIcon(Resources::GetThemeIcon("play"));
+ }
+ else
+ {
+ breakpoints.EnableBreaking(true);
+ memchecks.EnableBreaking(true);
+ has_memory_bp = memchecks.HasAny();
+ m_enabled->setText(tr("Disable"));
+ m_enabled->setIcon(Resources::GetThemeIcon("pause"));
+ }
+
+ if (has_memory_bp || !breakpoints.GetBreakPoints().empty())
+ emit Host::GetInstance()->PPCBreakpointsChanged();
+}
+
void BreakpointWidget::OnLoad()
{
Common::IniFile ini;
diff --git a/Source/Core/DolphinQt/Debugger/BreakpointWidget.h b/Source/Core/DolphinQt/Debugger/BreakpointWidget.h
index f48570f449..64dfcf1283 100644
--- a/Source/Core/DolphinQt/Debugger/BreakpointWidget.h
+++ b/Source/Core/DolphinQt/Debugger/BreakpointWidget.h
@@ -58,6 +58,7 @@ private:
void OnClear();
void OnClicked(QTableWidgetItem* item);
+ void OnToggleBreaking();
void OnNewBreakpoint();
void OnEditBreakpoint(u32 address, bool is_instruction_bp);
void OnLoad();
@@ -70,6 +71,7 @@ private:
QToolBar* m_toolbar;
QTableWidget* m_table;
+ QAction* m_enabled;
QAction* m_new;
QAction* m_clear;
QAction* m_load;
diff --git a/Source/Core/DolphinQt/Debugger/CodeViewWidget.cpp b/Source/Core/DolphinQt/Debugger/CodeViewWidget.cpp
index ce02b6baa5..c7a74ee29d 100644
--- a/Source/Core/DolphinQt/Debugger/CodeViewWidget.cpp
+++ b/Source/Core/DolphinQt/Debugger/CodeViewWidget.cpp
@@ -319,6 +319,7 @@ void CodeViewWidget::Update(const Core::CPUThreadGuard* guard)
guard ? std::make_optional(power_pc.GetPPCState().pc) : std::nullopt;
const bool dark_theme = Settings::Instance().IsThemeDark();
+ const bool breaking_enabled = power_pc.GetBreakPoints().IsBreakingEnabled();
m_branches.clear();
@@ -400,7 +401,7 @@ void CodeViewWidget::Update(const Core::CPUThreadGuard* guard)
if (bp != nullptr)
{
auto icon = Resources::GetThemeIcon("debugger_breakpoint").pixmap(QSize(rowh - 2, rowh - 2));
- if (!bp->is_enabled)
+ if (!breaking_enabled || !bp->is_enabled)
{
QPixmap disabled_icon(icon.size());
disabled_icon.fill(Qt::transparent);
diff --git a/Source/Core/DolphinQt/Debugger/NetworkWidget.cpp b/Source/Core/DolphinQt/Debugger/NetworkWidget.cpp
index d7ca4c8dc2..c32f55c1b5 100644
--- a/Source/Core/DolphinQt/Debugger/NetworkWidget.cpp
+++ b/Source/Core/DolphinQt/Debugger/NetworkWidget.cpp
@@ -21,9 +21,13 @@
#include <sys/socket.h>
#endif
+#include <bit>
+
#include "Common/FileUtil.h"
+#include "Common/Network.h"
#include "Core/Config/MainSettings.h"
#include "Core/Core.h"
+#include "Core/HW/DVD/AMMediaboard.h"
#include "Core/IOS/Network/SSL.h"
#include "Core/IOS/Network/Socket.h"
#include "Core/System.h"
@@ -96,7 +100,7 @@ QTableWidgetItem* GetSocketState(s32 host_fd)
return new QTableWidgetItem(QTableWidget::tr("Unbound"));
}
-static QTableWidgetItem* GetSocketBlocking(const IOS::HLE::WiiSockMan& socket_manager, s32 wii_fd)
+QTableWidgetItem* GetSocketBlocking(const IOS::HLE::WiiSockMan& socket_manager, s32 wii_fd)
{
if (socket_manager.GetHostSocket(wii_fd) < 0)
return new QTableWidgetItem();
@@ -104,7 +108,7 @@ static QTableWidgetItem* GetSocketBlocking(const IOS::HLE::WiiSockMan& socket_ma
return new QTableWidgetItem(is_blocking ? QTableWidget::tr("Yes") : QTableWidget::tr("No"));
}
-static QString GetAddressAndPort(const sockaddr_in& addr)
+QString GetAddressAndPort(const sockaddr_in& addr)
{
char buffer[16];
const char* addr_str = inet_ntop(AF_INET, &addr.sin_addr, buffer, sizeof(buffer));
@@ -139,6 +143,50 @@ QTableWidgetItem* GetSocketName(s32 host_fd)
return new QTableWidgetItem(QStringLiteral("%1->%2").arg(sock_name).arg(peer_name));
}
+
+QTableWidgetItem* GetSocketRedirections(s32 host_fd,
+ const AMMediaboard::IPRedirections& ip_redirections)
+{
+ if (host_fd < 0 || ip_redirections.empty())
+ return new QTableWidgetItem();
+
+ sockaddr_in sock_addr;
+ socklen_t sock_addr_len = sizeof(sockaddr_in);
+ if (getsockname(host_fd, reinterpret_cast<sockaddr*>(&sock_addr), &sock_addr_len) != 0)
+ return new QTableWidgetItem();
+ const Common::IPv4Port sock_ip_port{std::bit_cast<Common::IPAddress>(sock_addr.sin_addr),
+ sock_addr.sin_port};
+
+ sockaddr_in peer_addr;
+ socklen_t peer_addr_len = sizeof(sockaddr_in);
+ bool has_peer =
+ getpeername(host_fd, reinterpret_cast<sockaddr*>(&peer_addr), &peer_addr_len) == 0;
+ const Common::IPv4Port peer_ip_port =
+ has_peer ? Common::IPv4Port{std::bit_cast<Common::IPAddress>(peer_addr.sin_addr),
+ peer_addr.sin_port} :
+ Common::IPv4Port{};
+
+ QStringList sock_rules;
+ QStringList peer_rules;
+ for (const auto& rule : ip_redirections)
+ {
+ if (rule.replacement.IsMatch(sock_ip_port))
+ sock_rules << QString::fromStdString(rule.ToString());
+ if (!has_peer)
+ continue;
+ if (rule.replacement.IsMatch(peer_ip_port))
+ peer_rules << QString::fromStdString(rule.ToString());
+ }
+
+ if (sock_rules.isEmpty() && peer_rules.isEmpty())
+ return new QTableWidgetItem();
+ if (peer_rules.isEmpty())
+ return new QTableWidgetItem(sock_rules.join(QStringLiteral("\n")));
+ return new QTableWidgetItem(QStringLiteral("Sock rules:\n%1\n"
+ "\nPeer rules:%2")
+ .arg(sock_rules.join(QStringLiteral("\n")))
+ .arg(peer_rules.join(QStringLiteral("\n"))));
+}
} // namespace
NetworkWidget::NetworkWidget(QWidget* parent) : QDockWidget(parent)
@@ -184,7 +232,7 @@ void NetworkWidget::closeEvent(QCloseEvent*)
Settings::Instance().SetNetworkVisible(false);
}
-void NetworkWidget::showEvent(QShowEvent* event)
+void NetworkWidget::showEvent(QShowEvent*)
{
Update();
}
@@ -208,45 +256,34 @@ void NetworkWidget::ConnectWidgets()
{
connect(m_dump_format_combo, &QComboBox::currentIndexChanged, this,
&NetworkWidget::OnDumpFormatComboChanged);
+
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
- connect(m_dump_ssl_read_checkbox, &QCheckBox::checkStateChanged, [](Qt::CheckState state) {
- Config::SetBaseOrCurrent(Config::MAIN_NETWORK_SSL_DUMP_READ, state == Qt::Checked);
- });
- connect(m_dump_ssl_write_checkbox, &QCheckBox::checkStateChanged, [](Qt::CheckState state) {
- Config::SetBaseOrCurrent(Config::MAIN_NETWORK_SSL_DUMP_WRITE, state == Qt::Checked);
- });
- connect(m_dump_root_ca_checkbox, &QCheckBox::checkStateChanged, [](Qt::CheckState state) {
- Config::SetBaseOrCurrent(Config::MAIN_NETWORK_SSL_DUMP_ROOT_CA, state == Qt::Checked);
- });
- connect(m_dump_peer_cert_checkbox, &QCheckBox::checkStateChanged, [](Qt::CheckState state) {
- Config::SetBaseOrCurrent(Config::MAIN_NETWORK_SSL_DUMP_PEER_CERT, state == Qt::Checked);
- });
- connect(m_verify_certificates_checkbox, &QCheckBox::checkStateChanged, [](Qt::CheckState state) {
- Config::SetBaseOrCurrent(Config::MAIN_NETWORK_SSL_VERIFY_CERTIFICATES, state == Qt::Checked);
- });
- connect(m_dump_bba_checkbox, &QCheckBox::checkStateChanged, [](Qt::CheckState state) {
- Config::SetBaseOrCurrent(Config::MAIN_NETWORK_DUMP_BBA, state == Qt::Checked);
- });
+ using CheckState = Qt::CheckState;
+ static constexpr auto checkStateChanged = &QCheckBox::checkStateChanged;
#else
- connect(m_dump_ssl_read_checkbox, &QCheckBox::stateChanged, [](int state) {
+ using CheckState = int;
+ static constexpr auto checkStateChanged = &QCheckBox::stateChanged;
+#endif
+
+ connect(m_dump_ssl_read_checkbox, checkStateChanged, [](CheckState state) {
Config::SetBaseOrCurrent(Config::MAIN_NETWORK_SSL_DUMP_READ, state == Qt::Checked);
});
- connect(m_dump_ssl_write_checkbox, &QCheckBox::stateChanged, [](int state) {
+ connect(m_dump_ssl_write_checkbox, checkStateChanged, [](CheckState state) {
Config::SetBaseOrCurrent(Config::MAIN_NETWORK_SSL_DUMP_WRITE, state == Qt::Checked);
});
- connect(m_dump_root_ca_checkbox, &QCheckBox::stateChanged, [](int state) {
+ connect(m_dump_root_ca_checkbox, checkStateChanged, [](CheckState state) {
Config::SetBaseOrCurrent(Config::MAIN_NETWORK_SSL_DUMP_ROOT_CA, state == Qt::Checked);
});
- connect(m_dump_peer_cert_checkbox, &QCheckBox::stateChanged, [](int state) {
+ connect(m_dump_peer_cert_checkbox, checkStateChanged, [](CheckState state) {
Config::SetBaseOrCurrent(Config::MAIN_NETWORK_SSL_DUMP_PEER_CERT, state == Qt::Checked);
});
- connect(m_verify_certificates_checkbox, &QCheckBox::stateChanged, [](int state) {
+ connect(m_verify_certificates_checkbox, checkStateChanged, [](CheckState state) {
Config::SetBaseOrCurrent(Config::MAIN_NETWORK_SSL_VERIFY_CERTIFICATES, state == Qt::Checked);
});
- connect(m_dump_bba_checkbox, &QCheckBox::stateChanged, [](int state) {
+ connect(m_dump_bba_checkbox, checkStateChanged, [](CheckState state) {
Config::SetBaseOrCurrent(Config::MAIN_NETWORK_DUMP_BBA, state == Qt::Checked);
});
-#endif
+
connect(m_open_dump_folder, &QPushButton::clicked, [] {
const std::string location = File::GetUserPath(D_DUMPSSL_IDX);
const QUrl url = QUrl::fromLocalFile(QString::fromStdString(location));
@@ -254,24 +291,12 @@ void NetworkWidget::ConnectWidgets()
});
}
-void NetworkWidget::Update()
+void NetworkWidget::UpdateWiiSocketTable(Core::System& system)
{
- if (!isVisible())
- return;
-
- auto& system = Core::System::GetInstance();
- if (Core::GetState(system) != Core::State::Paused)
- {
- m_socket_table->setDisabled(true);
- m_ssl_table->setDisabled(true);
- return;
- }
-
- m_socket_table->setDisabled(false);
- m_ssl_table->setDisabled(false);
-
- // needed because there's a race condition on the IOS instance otherwise
- const Core::CPUThreadGuard guard(system);
+ // Show Wii socket blocking state
+ m_socket_table->showColumn(4);
+ // Hide Triforce IP redirections
+ m_socket_table->hideColumn(6);
auto* ios = system.GetIOS();
if (!ios)
@@ -281,7 +306,6 @@ void NetworkWidget::Update()
if (!socket_manager)
return;
- m_socket_table->setRowCount(0);
for (s32 wii_fd = 0; wii_fd < IOS::HLE::WII_SOCKET_FD_MAX; wii_fd++)
{
m_socket_table->insertRow(wii_fd);
@@ -293,9 +317,7 @@ void NetworkWidget::Update()
m_socket_table->setItem(wii_fd, 4, GetSocketBlocking(*socket_manager, wii_fd));
m_socket_table->setItem(wii_fd, 5, GetSocketName(host_fd));
}
- m_socket_table->resizeColumnsToContents();
- m_ssl_table->setRowCount(0);
for (s32 ssl_id = 0; ssl_id < IOS::HLE::NET_SSL_MAXINSTANCES; ssl_id++)
{
m_ssl_table->insertRow(ssl_id);
@@ -312,6 +334,58 @@ void NetworkWidget::Update()
m_ssl_table->setItem(ssl_id, 3, GetSocketState(host_fd));
m_ssl_table->setItem(ssl_id, 4, GetSocketName(host_fd));
}
+}
+
+void NetworkWidget::UpdateTriforceSocketTable()
+{
+ // No easy way to get socket blocking state on Windows
+ m_socket_table->hideColumn(4);
+ // Show active IP redirections
+ m_socket_table->showColumn(6);
+ const auto ip_redirections = AMMediaboard::GetIPRedirections();
+ for (s32 triforce_fd = 0; triforce_fd != AMMediaboard::SOCKET_FD_MAX; ++triforce_fd)
+ {
+ m_socket_table->insertRow(triforce_fd);
+ const s32 host_fd = AMMediaboard::DebuggerGetSocket(triforce_fd);
+ m_socket_table->setItem(triforce_fd, 0, new QTableWidgetItem(QString::number(triforce_fd)));
+ m_socket_table->setItem(triforce_fd, 1, GetSocketDomain(host_fd));
+ m_socket_table->setItem(triforce_fd, 2, GetSocketType(host_fd));
+ m_socket_table->setItem(triforce_fd, 3, GetSocketState(host_fd));
+ m_socket_table->setItem(triforce_fd, 4, new QTableWidgetItem(QTableWidget::tr("Unknown")));
+ m_socket_table->setItem(triforce_fd, 5, GetSocketName(host_fd));
+ m_socket_table->setItem(triforce_fd, 6, GetSocketRedirections(host_fd, ip_redirections));
+ }
+}
+
+void NetworkWidget::Update()
+{
+ if (!isVisible())
+ return;
+
+ auto& system = Core::System::GetInstance();
+ if (Core::GetState(system) != Core::State::Paused)
+ {
+ m_socket_table->setDisabled(true);
+ m_ssl_table->setDisabled(true);
+ return;
+ }
+
+ // needed because there's a race condition on the IOS instance otherwise
+ const Core::CPUThreadGuard guard(system);
+ m_socket_table->setDisabled(false);
+ m_socket_table->setRowCount(0);
+ m_ssl_table->setRowCount(0);
+ if (system.IsTriforce())
+ {
+ UpdateTriforceSocketTable();
+ }
+ else if (system.IsWii())
+ {
+ m_ssl_table->setDisabled(false);
+ UpdateWiiSocketTable(system);
+ }
+ m_socket_table->resizeColumnsToContents();
+ m_socket_table->resizeRowsToContents();
m_ssl_table->resizeColumnsToContents();
const bool is_pcap = Config::Get(Config::MAIN_NETWORK_DUMP_AS_PCAP);
@@ -328,27 +402,31 @@ void NetworkWidget::Update()
const int combo_index = int([is_pcap, is_ssl_read, is_ssl_write]() -> FormatComboId {
if (is_pcap)
return FormatComboId::PCAP;
- else if (is_ssl_read && is_ssl_write)
+
+ if (is_ssl_read && is_ssl_write)
return FormatComboId::BinarySSL;
- else if (is_ssl_read)
+
+ if (is_ssl_read)
return FormatComboId::BinarySSLRead;
- else if (is_ssl_write)
+
+ if (is_ssl_write)
return FormatComboId::BinarySSLWrite;
- else
- return FormatComboId::None;
+
+ return FormatComboId::None;
}());
m_dump_format_combo->setCurrentIndex(combo_index);
}
QGroupBox* NetworkWidget::CreateSocketTableGroup()
{
- QGroupBox* socket_table_group = new QGroupBox(tr("Socket table"));
- QGridLayout* socket_table_layout = new QGridLayout;
+ auto* const socket_table_group = new QGroupBox(tr("Socket table"));
+ auto* const socket_table_layout = new QGridLayout;
socket_table_group->setLayout(socket_table_layout);
m_socket_table = new QTableWidget();
// i18n: FD stands for file descriptor (and in this case refers to sockets, not regular files)
- QStringList header{tr("FD"), tr("Domain"), tr("Type"), tr("State"), tr("Blocking"), tr("Name")};
+ QStringList header{tr("FD"), tr("Domain"), tr("Type"), tr("State"),
+ tr("Blocking"), tr("Name"), tr("Redirections")};
m_socket_table->setColumnCount(static_cast<int>(header.size()));
m_socket_table->setHorizontalHeaderLabels(header);
@@ -365,8 +443,8 @@ QGroupBox* NetworkWidget::CreateSocketTableGroup()
QGroupBox* NetworkWidget::CreateSSLContextGroup()
{
- QGroupBox* ssl_context_group = new QGroupBox(tr("SSL context"));
- QGridLayout* ssl_context_layout = new QGridLayout;
+ auto* const ssl_context_group = new QGroupBox(tr("SSL context"));
+ auto* const ssl_context_layout = new QGridLayout;
ssl_context_group->setLayout(ssl_context_layout);
m_ssl_table = new QTableWidget();
@@ -387,8 +465,8 @@ QGroupBox* NetworkWidget::CreateSSLContextGroup()
QGroupBox* NetworkWidget::CreateDumpOptionsGroup()
{
- auto* dump_options_group = new QGroupBox(tr("Dump options"));
- auto* dump_options_layout = new QVBoxLayout;
+ auto* const dump_options_group = new QGroupBox(tr("Dump options"));
+ auto* const dump_options_layout = new QVBoxLayout;
dump_options_group->setLayout(dump_options_layout);
m_dump_format_combo = CreateDumpFormatCombo();
@@ -402,9 +480,9 @@ QGroupBox* NetworkWidget::CreateDumpOptionsGroup()
m_open_dump_folder = new QPushButton(tr("Open dump folder"));
m_open_dump_folder->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
- auto* combo_label = new QLabel(tr("Network dump format:"));
+ auto* const combo_label = new QLabel(tr("Network dump format:"));
combo_label->setBuddy(m_dump_format_combo);
- auto* combo_layout = new QHBoxLayout;
+ auto* const combo_layout = new QHBoxLayout;
combo_layout->addWidget(combo_label);
const int combo_label_space =
combo_label->fontMetrics().boundingRect(QStringLiteral("__")).width();
@@ -426,8 +504,8 @@ QGroupBox* NetworkWidget::CreateDumpOptionsGroup()
QGroupBox* NetworkWidget::CreateSecurityOptionsGroup()
{
- auto* security_options_group = new QGroupBox(tr("Security options"));
- auto* security_options_layout = new QVBoxLayout;
+ auto* const security_options_group = new QGroupBox(tr("Security options"));
+ auto* const security_options_layout = new QVBoxLayout;
security_options_group->setLayout(security_options_layout);
m_verify_certificates_checkbox = new QCheckBox(tr("Verify certificates"));
diff --git a/Source/Core/DolphinQt/Debugger/NetworkWidget.h b/Source/Core/DolphinQt/Debugger/NetworkWidget.h
index cfcf50a5c7..c2bb13e1d6 100644
--- a/Source/Core/DolphinQt/Debugger/NetworkWidget.h
+++ b/Source/Core/DolphinQt/Debugger/NetworkWidget.h
@@ -14,6 +14,11 @@ class QShowEvent;
class QTableWidget;
class QTableWidgetItem;
+namespace Core
+{
+class System;
+}
+
class NetworkWidget : public QDockWidget
{
Q_OBJECT
@@ -29,6 +34,8 @@ private:
void CreateWidgets();
void ConnectWidgets();
+ void UpdateWiiSocketTable(Core::System& system);
+ void UpdateTriforceSocketTable();
void Update();
QGroupBox* CreateSocketTableGroup();
diff --git a/Source/Core/DolphinQt/DolphinQt.vcxproj b/Source/Core/DolphinQt/DolphinQt.vcxproj
index 6f580bbc60..a9459c971a 100644
--- a/Source/Core/DolphinQt/DolphinQt.vcxproj
+++ b/Source/Core/DolphinQt/DolphinQt.vcxproj
@@ -224,6 +224,7 @@
<ClCompile Include="Settings\GeneralPane.cpp" />
<ClCompile Include="Settings\InterfacePane.cpp" />
<ClCompile Include="Settings\OnScreenDisplayPane.cpp" />
+ <ClCompile Include="Settings\TriforcePane.cpp" />
<ClCompile Include="Settings\PathPane.cpp" />
<ClCompile Include="Settings\USBDevicePicker.cpp" />
<ClCompile Include="Settings\WiiPane.cpp" />
@@ -441,6 +442,7 @@
<QtMoc Include="Settings\GeneralPane.h" />
<QtMoc Include="Settings\InterfacePane.h" />
<QtMoc Include="Settings\PathPane.h" />
+ <QtMoc Include="Settings\TriforcePane.h" />
<QtMoc Include="Settings\USBDevicePicker.h" />
<QtMoc Include="Settings\WiiPane.h" />
<QtMoc Include="SkylanderPortal\SkylanderPortalWindow.h" />
@@ -525,4 +527,4 @@
<Message Text="Copy: @(BinaryFiles) -&gt; $(BinaryOutputDir)" Importance="High" />
<Copy SourceFiles="@(BinaryFiles)" DestinationFolder="$(BinaryOutputDir)" />
</Target>
-</Project> \ No newline at end of file
+</Project>
diff --git a/Source/Core/DolphinQt/MainWindow.cpp b/Source/Core/DolphinQt/MainWindow.cpp
index 9f25e66ad4..962f75e96c 100644
--- a/Source/Core/DolphinQt/MainWindow.cpp
+++ b/Source/Core/DolphinQt/MainWindow.cpp
@@ -1151,6 +1151,32 @@ void MainWindow::StartGame(std::unique_ptr<BootParameters>&& parameters)
if (!NKitWarningDialog::ShowUnlessDisabled())
return;
}
+
+ const auto volume_type =
+ std::get<BootParameters::Disc>(parameters->parameters).volume->GetVolumeType();
+ if (volume_type != DiscIO::Platform::Triforce)
+ {
+ const bool triforce_hardware_sp1 =
+ Config::Get(Config::MAIN_SERIAL_PORT_1) == ExpansionInterface::EXIDeviceType::Baseboard;
+ const bool triforce_hardware_port_1 = Config::Get(Config::GetInfoForSIDevice(0)) ==
+ SerialInterface::SIDevices::SIDEVICE_AM_BASEBOARD;
+
+ // Some Triforce tools don't include a boot.id file, but they can still be launched.
+ if (triforce_hardware_sp1)
+ {
+ ModalMessageBox::warning(this, tr("Warning"),
+ tr("Non-Triforce games cannot be booted with Triforce hardware "
+ "attached.\nPlease remove the Triforce Baseboard from SP1."),
+ QMessageBox::Ok);
+ }
+ if (triforce_hardware_port_1)
+ {
+ ModalMessageBox::warning(this, tr("Warning"),
+ tr("Non-Triforce games cannot be booted with Triforce hardware "
+ "attached.\nPlease remove the Triforce Baseboard from Port 1."),
+ QMessageBox::Ok);
+ }
+ }
}
// If we're running, only start a new game once we've stopped the last.
@@ -1289,6 +1315,12 @@ void MainWindow::ShowControllersWindow()
m_settings_window->SelectPane(SettingsWindowPaneIndex::Controllers);
}
+void MainWindow::ShowTriforceWindow()
+{
+ ShowSettingsWindow();
+ m_settings_window->SelectPane(SettingsWindowPaneIndex::Triforce);
+}
+
void MainWindow::ShowFreeLookWindow()
{
if (!m_freelook_window)
diff --git a/Source/Core/DolphinQt/MainWindow.h b/Source/Core/DolphinQt/MainWindow.h
index bb7307c98c..c0d5bd110e 100644
--- a/Source/Core/DolphinQt/MainWindow.h
+++ b/Source/Core/DolphinQt/MainWindow.h
@@ -92,6 +92,8 @@ public:
bool eventFilter(QObject* object, QEvent* event) override;
QMenu* createPopupMenu() override;
+ void ShowTriforceWindow();
+
signals:
void ReadOnlyModeChanged(bool read_only);
void RecordingStatusChanged(bool recording);
diff --git a/Source/Core/DolphinQt/MenuBar.cpp b/Source/Core/DolphinQt/MenuBar.cpp
index cef4a6a28c..04c1b3eecd 100644
--- a/Source/Core/DolphinQt/MenuBar.cpp
+++ b/Source/Core/DolphinQt/MenuBar.cpp
@@ -319,6 +319,9 @@ void MenuBar::AddToolsMenu()
m_pal_ipl =
gc_ipl->addAction(tr("PAL"), this, [this] { emit BootGameCubeIPL(DiscIO::Region::PAL); });
+ m_dev_ipl = gc_ipl->addAction(tr("Triforce"), this,
+ [this] { emit BootGameCubeIPL(DiscIO::Region::Unknown); });
+
tools_menu->addAction(tr("Memory Card Manager"), this, [this] { emit ShowMemcardManager(); });
tools_menu->addSeparator();
@@ -1091,6 +1094,7 @@ void MenuBar::UpdateToolsMenu(const Core::State state)
m_ntscj_ipl->setEnabled(is_uninitialized && File::Exists(Config::GetBootROMPath(JAP_DIR)));
m_ntscu_ipl->setEnabled(is_uninitialized && File::Exists(Config::GetBootROMPath(USA_DIR)));
m_pal_ipl->setEnabled(is_uninitialized && File::Exists(Config::GetBootROMPath(EUR_DIR)));
+ m_dev_ipl->setEnabled(is_uninitialized && File::Exists(Config::GetBootROMPath(DEV_DIR)));
m_wad_install_action->setEnabled(is_uninitialized);
m_import_backup->setEnabled(is_uninitialized);
m_check_nand->setEnabled(is_uninitialized);
diff --git a/Source/Core/DolphinQt/MenuBar.h b/Source/Core/DolphinQt/MenuBar.h
index 9f51179e2a..2cd56f4e54 100644
--- a/Source/Core/DolphinQt/MenuBar.h
+++ b/Source/Core/DolphinQt/MenuBar.h
@@ -227,6 +227,7 @@ private:
QAction* m_ntscj_ipl;
QAction* m_ntscu_ipl;
QAction* m_pal_ipl;
+ QAction* m_dev_ipl;
QMenu* m_manage_nand_menu;
QAction* m_import_backup;
QAction* m_check_nand;
diff --git a/Source/Core/DolphinQt/Settings/AdvancedPane.cpp b/Source/Core/DolphinQt/Settings/AdvancedPane.cpp
index b96d2b24ea..ddb47bc767 100644
--- a/Source/Core/DolphinQt/Settings/AdvancedPane.cpp
+++ b/Source/Core/DolphinQt/Settings/AdvancedPane.cpp
@@ -156,7 +156,7 @@ void AdvancedPane::CreateLayout()
cpu_clock_override_slider_layout->setContentsMargins(0, 0, 0, 0);
clock_override_layout->addLayout(cpu_clock_override_slider_layout);
- m_cpu_clock_override_slider = new ConfigFloatSlider(0.01f, 4.0f, Config::MAIN_OVERCLOCK, 0.01f);
+ m_cpu_clock_override_slider = new ConfigFloatSlider(0.01f, 5.0f, Config::MAIN_OVERCLOCK, 0.01f);
cpu_clock_override_slider_layout->addWidget(m_cpu_clock_override_slider);
m_cpu_label = new QLabel();
diff --git a/Source/Core/DolphinQt/Settings/GameCubePane.cpp b/Source/Core/DolphinQt/Settings/GameCubePane.cpp
index 772ade109e..b4ee1f25ad 100644
--- a/Source/Core/DolphinQt/Settings/GameCubePane.cpp
+++ b/Source/Core/DolphinQt/Settings/GameCubePane.cpp
@@ -31,6 +31,7 @@
#include "Core/Core.h"
#include "Core/HW/EXI/EXI.h"
#include "Core/HW/GCMemcard/GCMemcard.h"
+#include "DolphinQt/Config/SettingsWindow.h"
#ifdef HAS_LIBMGBA
#include "Core/NetPlayServer.h"
#endif
@@ -50,15 +51,18 @@
#include "DolphinQt/QtUtils/SignalBlocking.h"
#include "DolphinQt/Settings.h"
#include "DolphinQt/Settings/BroadbandAdapterSettingsDialog.h"
+#include "DolphinQt/Settings/TriforcePane.h"
constexpr std::initializer_list<ExpansionInterface::Slot> GUI_SLOTS = {
ExpansionInterface::Slot::A, ExpansionInterface::Slot::B, ExpansionInterface::Slot::SP1};
-GameCubePane::GameCubePane()
+GameCubePane::GameCubePane(MainWindow* main_window)
{
CreateWidgets();
LoadSettings();
ConnectWidgets();
+
+ connect(this, &GameCubePane::ShowTriforceWindow, main_window, &MainWindow::ShowTriforceWindow);
}
void GameCubePane::CreateWidgets()
@@ -151,6 +155,7 @@ void GameCubePane::CreateWidgets()
EXIDeviceType::EthernetIPC,
#endif
EXIDeviceType::ModemTapServer,
+ EXIDeviceType::Baseboard,
})
{
m_slot_combos[ExpansionInterface::Slot::SP1]->addItem(tr(fmt::format("{:n}", device).c_str()),
@@ -350,11 +355,12 @@ void GameCubePane::UpdateButton(ExpansionInterface::Slot slot)
break;
}
case ExpansionInterface::Slot::SP1:
- has_config = (device == ExpansionInterface::EXIDeviceType::Ethernet ||
- device == ExpansionInterface::EXIDeviceType::EthernetXLink ||
- device == ExpansionInterface::EXIDeviceType::EthernetTapServer ||
- device == ExpansionInterface::EXIDeviceType::EthernetBuiltIn ||
- device == ExpansionInterface::EXIDeviceType::ModemTapServer);
+ has_config = device == ExpansionInterface::EXIDeviceType::Ethernet ||
+ device == ExpansionInterface::EXIDeviceType::EthernetXLink ||
+ device == ExpansionInterface::EXIDeviceType::EthernetTapServer ||
+ device == ExpansionInterface::EXIDeviceType::EthernetBuiltIn ||
+ device == ExpansionInterface::EXIDeviceType::ModemTapServer ||
+ device == ExpansionInterface::EXIDeviceType::Baseboard;
break;
case ExpansionInterface::Slot::SP2:
has_config = false;
@@ -418,6 +424,11 @@ void GameCubePane::OnConfigPressed(ExpansionInterface::Slot slot)
dialog.exec();
return;
}
+ case ExpansionInterface::EXIDeviceType::Baseboard:
+ {
+ ShowTriforceWindow();
+ return;
+ }
default:
PanicAlertFmt("Unknown settings pressed for {}", device);
return;
@@ -452,19 +463,23 @@ bool GameCubePane::SetMemcard(ExpansionInterface::Slot slot, const QString& file
const std::string jp_path = Config::GetMemcardPath(raw_path, slot, DiscIO::Region::NTSC_J);
const std::string us_path = Config::GetMemcardPath(raw_path, slot, DiscIO::Region::NTSC_U);
const std::string eu_path = Config::GetMemcardPath(raw_path, slot, DiscIO::Region::PAL);
- const bool raw_path_valid = raw_path == jp_path || raw_path == us_path || raw_path == eu_path;
+ const std::string dv_path = Config::GetMemcardPath(raw_path, slot, DiscIO::Region::DEV);
+ const bool raw_path_valid =
+ raw_path == jp_path || raw_path == us_path || raw_path == eu_path || raw_path == dv_path;
if (!raw_path_valid)
{
// TODO: We could try to autodetect the card region here and offer automatic renaming.
- ModalMessageBox::critical(this, tr("Error"),
- tr("The filename %1 does not conform to Dolphin's region code format "
- "for memory cards. Please rename this file to either %2, %3, or "
- "%4, matching the region of the save files that are on it.")
- .arg(QString::fromStdString(PathToFileName(raw_path)))
- .arg(QString::fromStdString(PathToFileName(us_path)))
- .arg(QString::fromStdString(PathToFileName(eu_path)))
- .arg(QString::fromStdString(PathToFileName(jp_path))));
+ ModalMessageBox::critical(
+ this, tr("Error"),
+ tr("The filename %1 does not conform to Dolphin's region code format "
+ "for memory cards. Please rename this file to either %2, %3, %4, or "
+ "%5, matching the region of the save files that are on it.")
+ .arg(QString::fromStdString(PathToFileName(raw_path)))
+ .arg(QString::fromStdString(PathToFileName(us_path)))
+ .arg(QString::fromStdString(PathToFileName(eu_path)))
+ .arg(QString::fromStdString(PathToFileName(jp_path)))
+ .arg(QString::fromStdString(PathToFileName(dv_path))));
return false;
}
@@ -556,8 +571,9 @@ bool GameCubePane::SetGCIFolder(ExpansionInterface::Slot slot, const QString& pa
const std::string default_jp_path = Config::GetGCIFolderPath("", slot, DiscIO::Region::NTSC_J);
const std::string default_us_path = Config::GetGCIFolderPath("", slot, DiscIO::Region::NTSC_U);
const std::string default_eu_path = Config::GetGCIFolderPath("", slot, DiscIO::Region::PAL);
- const bool is_default_path =
- raw_path == default_jp_path || raw_path == default_us_path || raw_path == default_eu_path;
+ const std::string default_dv_path = Config::GetGCIFolderPath("", slot, DiscIO::Region::DEV);
+ const bool is_default_path = raw_path == default_jp_path || raw_path == default_us_path ||
+ raw_path == default_eu_path || raw_path == default_dv_path;
bool path_changed;
if (is_default_path)
@@ -574,7 +590,9 @@ bool GameCubePane::SetGCIFolder(ExpansionInterface::Slot slot, const QString& pa
const std::string jp_path = Config::GetGCIFolderPath(raw_path, slot, DiscIO::Region::NTSC_J);
const std::string us_path = Config::GetGCIFolderPath(raw_path, slot, DiscIO::Region::NTSC_U);
const std::string eu_path = Config::GetGCIFolderPath(raw_path, slot, DiscIO::Region::PAL);
- const bool raw_path_valid = raw_path == jp_path || raw_path == us_path || raw_path == eu_path;
+ const std::string dv_path = Config::GetGCIFolderPath(raw_path, slot, DiscIO::Region::DEV);
+ const bool raw_path_valid =
+ raw_path == jp_path || raw_path == us_path || raw_path == eu_path || raw_path == dv_path;
if (!raw_path_valid)
{
@@ -582,12 +600,13 @@ bool GameCubePane::SetGCIFolder(ExpansionInterface::Slot slot, const QString& pa
ModalMessageBox::critical(
this, tr("Error"),
tr("The folder %1 does not conform to Dolphin's region code format "
- "for GCI folders. Please rename this folder to either %2, %3, or "
- "%4, matching the region of the save files that are in it.")
+ "for GCI folders. Please rename this folder to either %2, %3, %4, or "
+ "%5, matching the region of the save files that are in it.")
.arg(QString::fromStdString(PathToFileName(raw_path)))
.arg(QString::fromStdString(PathToFileName(us_path)))
.arg(QString::fromStdString(PathToFileName(eu_path)))
- .arg(QString::fromStdString(PathToFileName(jp_path))));
+ .arg(QString::fromStdString(PathToFileName(jp_path)))
+ .arg(QString::fromStdString(PathToFileName(dv_path))));
return false;
}
diff --git a/Source/Core/DolphinQt/Settings/GameCubePane.h b/Source/Core/DolphinQt/Settings/GameCubePane.h
index bf7c7c2e6d..a97e82af3f 100644
--- a/Source/Core/DolphinQt/Settings/GameCubePane.h
+++ b/Source/Core/DolphinQt/Settings/GameCubePane.h
@@ -11,6 +11,7 @@
#include "Common/EnumMap.h"
#include "Core/HW/EXI/EXI.h"
+#include "DolphinQt/MainWindow.h"
class ConfigBool;
class ConfigChoice;
@@ -28,10 +29,13 @@ class GameCubePane : public QWidget
{
Q_OBJECT
public:
- explicit GameCubePane();
+ explicit GameCubePane(MainWindow* main_window);
static std::string GetOpenGBARom(std::string_view title);
+signals:
+ void ShowTriforceWindow();
+
private:
void CreateWidgets();
void ConnectWidgets();
diff --git a/Source/Core/DolphinQt/Settings/TriforcePane.cpp b/Source/Core/DolphinQt/Settings/TriforcePane.cpp
new file mode 100644
index 0000000000..025bbe0bd1
--- /dev/null
+++ b/Source/Core/DolphinQt/Settings/TriforcePane.cpp
@@ -0,0 +1,221 @@
+// Copyright 2026 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "DolphinQt/Settings/TriforcePane.h"
+
+#include <QDialogButtonBox>
+#include <QGroupBox>
+#include <QHeaderView>
+#include <QPushButton>
+#include <QTableWidget>
+#include <QVBoxLayout>
+
+#include <fmt/format.h>
+#include <fmt/ranges.h>
+
+#include "Common/Logging/Log.h"
+
+#include "Core/Config/MainSettings.h"
+#include "Core/HW/DVD/AMMediaboard.h"
+
+#include "DolphinQt/Config/Mapping/MappingWindow.h"
+#include "DolphinQt/QtUtils/NonDefaultQPushButton.h"
+#include "DolphinQt/QtUtils/QtUtils.h"
+
+namespace
+{
+
+constexpr int COLUMN_EMULATED = 0;
+constexpr int COLUMN_REAL = 1;
+constexpr int COLUMN_DESCRIPTION = 2;
+
+class IPRedirectionsDialog final : public QDialog
+{
+public:
+ explicit IPRedirectionsDialog(QWidget* parent);
+
+private:
+ void LoadConfig();
+ void SaveConfig() const;
+
+ void LoadDefault();
+ void OnClear();
+
+ QTableWidget* m_ip_redirections_table;
+};
+
+} // namespace
+
+TriforcePane::TriforcePane()
+{
+ auto* const main_layout = new QVBoxLayout{this};
+
+ auto* const controllers_group = new QGroupBox{tr("Controllers")};
+ main_layout->addWidget(controllers_group);
+
+ auto* const controllers_layout = new QVBoxLayout{controllers_group};
+
+ auto* const configure_controllers_button = new NonDefaultQPushButton{tr("Configure")};
+ controllers_layout->addWidget(configure_controllers_button);
+
+ connect(configure_controllers_button, &QPushButton::clicked, this, [this] {
+ auto* const window = new MappingWindow(this, MappingWindow::Type::MAPPING_AM_BASEBOARD, 0);
+ window->setAttribute(Qt::WA_DeleteOnClose, true);
+ window->setWindowModality(Qt::WindowModality::WindowModal);
+ window->show();
+ });
+
+ auto* const ip_redirection_group = new QGroupBox{tr("IP Address Redirections")};
+ main_layout->addWidget(ip_redirection_group);
+
+ auto* const ip_redirection_layout = new QVBoxLayout{ip_redirection_group};
+
+ auto* const configure_ip_redirections_button = new NonDefaultQPushButton{tr("Configure")};
+ ip_redirection_layout->addWidget(configure_ip_redirections_button);
+
+ connect(configure_ip_redirections_button, &QPushButton::clicked, this, [this] {
+ auto* const ip_redirections = new IPRedirectionsDialog{this};
+ ip_redirections->setAttribute(Qt::WA_DeleteOnClose);
+ ip_redirections->open();
+ });
+
+ main_layout->addStretch(1);
+}
+
+IPRedirectionsDialog::IPRedirectionsDialog(QWidget* parent) : QDialog{parent}
+{
+ setWindowTitle(tr("IP Address Redirections"));
+
+ auto* const main_layout = new QVBoxLayout{this};
+
+ auto* const load_default_button = new QPushButton(tr("Default"));
+ connect(load_default_button, &QPushButton::clicked, this, &IPRedirectionsDialog::LoadDefault);
+
+ auto* const clear_button = new QPushButton(tr("Clear"));
+ connect(clear_button, &QPushButton::clicked, this, &IPRedirectionsDialog::OnClear);
+
+ m_ip_redirections_table = new QTableWidget;
+ main_layout->addWidget(m_ip_redirections_table);
+
+ m_ip_redirections_table->setColumnCount(3);
+ m_ip_redirections_table->setHorizontalHeaderLabels(
+ {tr("Emulated"), tr("Real"), tr("Description")});
+ m_ip_redirections_table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
+ m_ip_redirections_table->setSizeAdjustPolicy(QTableWidget::AdjustToContents);
+
+ m_ip_redirections_table->setEditTriggers(QAbstractItemView::DoubleClicked |
+ QAbstractItemView::EditKeyPressed);
+
+ auto* const button_box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+ main_layout->addWidget(button_box);
+
+ button_box->addButton(load_default_button, QDialogButtonBox::ActionRole);
+ button_box->addButton(clear_button, QDialogButtonBox::ActionRole);
+
+ connect(button_box, &QDialogButtonBox::accepted, this, &IPRedirectionsDialog::accept);
+ connect(button_box, &QDialogButtonBox::rejected, this, &IPRedirectionsDialog::reject);
+
+ connect(this, &IPRedirectionsDialog::accepted, this, &IPRedirectionsDialog::SaveConfig);
+
+ connect(m_ip_redirections_table, &QTableWidget::itemChanged, this,
+ [this](QTableWidgetItem* item) {
+ const int row_count = m_ip_redirections_table->rowCount();
+ const bool is_last_row = item->row() == row_count - 1;
+ if (is_last_row)
+ {
+ if (!item->text().isEmpty())
+ m_ip_redirections_table->insertRow(row_count);
+ }
+ else if (item->column() != COLUMN_DESCRIPTION && item->text().isEmpty())
+ {
+ // Just remove inner rows that have text erased.
+ // This UX could be less weird, but it's good enough for now.
+ m_ip_redirections_table->removeRow(item->row());
+ }
+ });
+
+ LoadConfig();
+
+ QtUtils::AdjustSizeWithinScreen(this);
+}
+
+void IPRedirectionsDialog::LoadConfig()
+{
+ m_ip_redirections_table->setRowCount(0);
+
+ // Add the final empty row.
+ m_ip_redirections_table->insertRow(0);
+
+ int row = 0;
+
+ const auto ip_redirections_str = Config::Get(Config::MAIN_TRIFORCE_IP_REDIRECTIONS);
+ for (auto&& ip_pair : ip_redirections_str | std::views::split(','))
+ {
+ const auto ip_pair_str = std::string_view{ip_pair};
+ const auto parsed = AMMediaboard::ParseIPRedirection(ip_pair_str);
+ if (!parsed.has_value())
+ {
+ ERROR_LOG_FMT(COMMON, "Bad IP redirection string: {}", ip_pair_str);
+ continue;
+ }
+
+ m_ip_redirections_table->insertRow(row);
+
+ m_ip_redirections_table->setItem(row, COLUMN_EMULATED,
+ new QTableWidgetItem(QString::fromUtf8(parsed->original)));
+ m_ip_redirections_table->setItem(row, COLUMN_REAL,
+ new QTableWidgetItem(QString::fromUtf8(parsed->replacement)));
+ m_ip_redirections_table->setItem(row, COLUMN_DESCRIPTION,
+ new QTableWidgetItem(QString::fromUtf8(parsed->description)));
+
+ ++row;
+ }
+}
+
+void IPRedirectionsDialog::SaveConfig() const
+{
+ // Ignore our empty row.
+ const int row_count = m_ip_redirections_table->rowCount() - 1;
+
+ std::vector<std::string> replacements(row_count);
+ for (int row = 0; row < row_count; ++row)
+ {
+ auto* const original_item = m_ip_redirections_table->item(row, COLUMN_EMULATED);
+ auto* const replacement_item = m_ip_redirections_table->item(row, COLUMN_REAL);
+
+ // Skip incomplete rows.
+ if (original_item == nullptr || replacement_item == nullptr)
+ continue;
+
+ replacements[row] = fmt::format("{}={}", StripWhitespace(original_item->text().toStdString()),
+ replacement_item->text().toStdString());
+
+ auto* const description_item = m_ip_redirections_table->item(row, COLUMN_DESCRIPTION);
+ if (description_item == nullptr)
+ continue;
+
+ const auto description = description_item->text().toStdString();
+
+ if (!description.empty())
+ (replacements[row] += ' ') += description;
+ }
+
+ Config::SetBaseOrCurrent(Config::MAIN_TRIFORCE_IP_REDIRECTIONS,
+ fmt::format("{}", fmt::join(replacements, ",")));
+}
+
+void IPRedirectionsDialog::LoadDefault()
+{
+ // This alters the config before "OK" is pressed. Bad UX..
+
+ Config::SetBaseOrCurrent(Config::MAIN_TRIFORCE_IP_REDIRECTIONS, Config::DefaultState{});
+
+ LoadConfig();
+}
+
+void IPRedirectionsDialog::OnClear()
+{
+ m_ip_redirections_table->setRowCount(0);
+ // Add the final empty row.
+ m_ip_redirections_table->insertRow(0);
+}
diff --git a/Source/Core/DolphinQt/Settings/TriforcePane.h b/Source/Core/DolphinQt/Settings/TriforcePane.h
new file mode 100644
index 0000000000..49038b3c5c
--- /dev/null
+++ b/Source/Core/DolphinQt/Settings/TriforcePane.h
@@ -0,0 +1,14 @@
+// Copyright 2026 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <QWidget>
+
+class TriforcePane final : public QWidget
+{
+ Q_OBJECT
+
+public:
+ TriforcePane();
+};
diff --git a/Source/Core/InputCommon/ControllerInterface/Android/Android.cpp b/Source/Core/InputCommon/ControllerInterface/Android/Android.cpp
index a0a3f7c8c2..0b0609f010 100644
--- a/Source/Core/InputCommon/ControllerInterface/Android/Android.cpp
+++ b/Source/Core/InputCommon/ControllerInterface/Android/Android.cpp
@@ -31,6 +31,8 @@
namespace
{
+std::string SOURCE = "Android";
+
jclass s_list_class;
jmethodID s_list_get;
jmethodID s_list_size;
@@ -468,13 +470,13 @@ namespace ciface::Android
class InputBackend final : public ciface::InputBackend
{
public:
- InputBackend(ControllerInterface* controller_interface);
+ explicit InputBackend(ControllerInterface* controller_interface);
~InputBackend();
void PopulateDevices() override;
-private:
- void AddDevice(JNIEnv* env, int device_id);
- void AddSensorDevice(JNIEnv* env);
+ static void AddDevice(JNIEnv* env, int device_id);
+ static void AddSensorDevice(JNIEnv* env);
+ static void RemoveDevice(int device_id);
};
std::unique_ptr<ciface::InputBackend> CreateInputBackend(ControllerInterface* controller_interface)
@@ -611,10 +613,11 @@ private:
class AndroidDevice final : public Core::Device
{
public:
- AndroidDevice(JNIEnv* env, jobject input_device)
+ AndroidDevice(JNIEnv* env, jint device_id, jobject input_device)
: m_sensor_event_listener(AddSensors(env, input_device)),
m_source(env->CallIntMethod(input_device, s_input_device_get_sources)),
- m_controller_number(env->CallIntMethod(input_device, s_input_device_get_controller_number))
+ m_controller_number(env->CallIntMethod(input_device, s_input_device_get_controller_number)),
+ m_device_id(device_id)
{
jstring j_name =
reinterpret_cast<jstring>(env->CallObjectMethod(input_device, s_input_device_get_name));
@@ -631,7 +634,7 @@ public:
// Constructor for the device added by Dolphin to contain sensor inputs
AndroidDevice(JNIEnv* env, std::string name)
: m_sensor_event_listener(AddSensors(env, nullptr)), m_source(AINPUT_SOURCE_SENSOR),
- m_controller_number(0), m_name(std::move(name))
+ m_controller_number(0), m_device_id(std::nullopt), m_name(std::move(name))
{
AddSystemMotors(env);
}
@@ -644,7 +647,7 @@ public:
std::string GetName() const override { return m_name; }
- std::string GetSource() const override { return "Android"; }
+ std::string GetSource() const override { return SOURCE; }
std::optional<int> GetPreferredId() const override
{
@@ -666,6 +669,8 @@ public:
return -3;
}
+ std::optional<jint> GetDeviceID() const { return m_device_id; }
+
jobject GetSensorEventListener() { return m_sensor_event_listener; }
private:
@@ -801,6 +806,7 @@ private:
const jobject m_sensor_event_listener;
const int m_source;
const int m_controller_number;
+ const std::optional<jint> m_device_id;
std::string m_name;
};
@@ -944,8 +950,12 @@ InputBackend::~InputBackend()
env->DeleteGlobalRef(s_keycodes_array);
}
-void InputBackend::AddDevice(JNIEnv* env, int device_id)
+void InputBackend::AddDevice(JNIEnv* env, jint device_id)
{
+ // Remove the device in case it already exists (maybe it's possible for a device to connect,
+ // be processed by PopulateDevices, and then be processed by onInputDeviceAdded)
+ RemoveDevice(device_id);
+
jobject input_device =
env->CallStaticObjectMethod(s_input_device_class, s_input_device_get_device, device_id);
@@ -955,14 +965,14 @@ void InputBackend::AddDevice(JNIEnv* env, int device_id)
return;
}
- auto device = std::make_shared<AndroidDevice>(env, input_device);
+ auto device = std::make_shared<AndroidDevice>(env, device_id, input_device);
env->DeleteLocalRef(input_device);
if (device->Inputs().empty() && device->Outputs().empty())
return;
- GetControllerInterface().AddDevice(device);
+ g_controller_interface.AddDevice(device);
Core::DeviceQualifier qualifier;
qualifier.FromDevice(device.get());
@@ -987,7 +997,7 @@ void InputBackend::AddSensorDevice(JNIEnv* env)
if (device->Inputs().empty() && device->Outputs().empty())
return;
- GetControllerInterface().AddDevice(device);
+ g_controller_interface.AddDevice(device);
Core::DeviceQualifier qualifier;
qualifier.FromDevice(device.get());
@@ -1000,6 +1010,16 @@ void InputBackend::AddSensorDevice(JNIEnv* env)
env->DeleteLocalRef(j_qualifier);
}
+void InputBackend::RemoveDevice(jint device_id)
+{
+ g_controller_interface.RemoveDevice([device_id](const ciface::Core::Device* device) {
+ return device->GetSource() == SOURCE &&
+ static_cast<const AndroidDevice*>(device)->GetDeviceID() == device_id;
+ });
+
+ s_device_id_to_device_qualifier.erase(device_id);
+}
+
void InputBackend::PopulateDevices()
{
INFO_LOG_FMT(CONTROLLERINTERFACE, "Android populating devices");
@@ -1163,10 +1183,25 @@ Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_notifySe
}
JNIEXPORT void JNICALL
-Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_refreshDevices(JNIEnv* env,
- jclass)
+Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_00024InputDeviceListener_onInputDeviceAdded(
+ JNIEnv* env, jobject, jint device_id)
+{
+ ciface::Android::InputBackend::AddDevice(env, device_id);
+}
+
+JNIEXPORT void JNICALL
+Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_00024InputDeviceListener_onInputDeviceRemoved(
+ JNIEnv*, jobject, jint device_id)
+{
+ ciface::Android::InputBackend::RemoveDevice(device_id);
+}
+
+JNIEXPORT void JNICALL
+Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_00024InputDeviceListener_onInputDeviceChanged(
+ JNIEnv* env, jobject, jint device_id)
{
- g_controller_interface.RefreshDevices();
+ // AddDevice will automatically remove the existing device
+ ciface::Android::InputBackend::AddDevice(env, device_id);
}
JNIEXPORT jobjectArray JNICALL
diff --git a/Source/Core/InputCommon/GCPadStatus.h b/Source/Core/InputCommon/GCPadStatus.h
index 74849e5594..b6bbf804b4 100644
--- a/Source/Core/InputCommon/GCPadStatus.h
+++ b/Source/Core/InputCommon/GCPadStatus.h
@@ -28,6 +28,13 @@ enum PadButton
PAD_BUTTON_START = 0x1000,
};
+enum SwitchButton
+{
+ SWITCH_TEST = 0x0001,
+ SWITCH_SERVICE = 0x0002,
+ SWITCH_COIN = 0x0004,
+};
+
struct GCPadStatus
{
u16 button = 0; // Or-ed PAD_BUTTON_* and PAD_TRIGGER_* bits
@@ -39,6 +46,8 @@ struct GCPadStatus
u8 triggerRight = 0; // 0 <= triggerRight <= 255
u8 analogA = 0; // 0 <= analogA <= 255
u8 analogB = 0; // 0 <= analogB <= 255
+ // Triforce
+ u8 switches = 0;
bool isConnected = true;
static const u8 MAIN_STICK_CENTER_X = 0x80;
diff --git a/Source/Core/UICommon/GameFile.cpp b/Source/Core/UICommon/GameFile.cpp
index 94ddf1f59c..4acab314d1 100644
--- a/Source/Core/UICommon/GameFile.cpp
+++ b/Source/Core/UICommon/GameFile.cpp
@@ -128,7 +128,6 @@ GameFile::GameFile(std::string path) : m_file_path(std::move(path))
m_internal_name = volume->GetInternalName();
m_game_id = volume->GetGameID();
m_gametdb_id = volume->GetGameTDBID();
- m_triforce_id = volume->GetTriforceID();
m_title_id = volume->GetTitleID().value_or(0);
m_maker_id = volume->GetMakerID();
m_revision = volume->GetRevision().value_or(0);
@@ -309,7 +308,6 @@ void GameFile::DoState(PointerWrap& p)
p.Do(m_internal_name);
p.Do(m_game_id);
p.Do(m_gametdb_id);
- p.Do(m_triforce_id);
p.Do(m_title_id);
p.Do(m_maker_id);
@@ -498,8 +496,7 @@ const std::string& GameFile::GetName(const Core::TitleDatabase& title_database)
if (IsModDescriptor())
return GetName(Variant::LongAndPossiblyCustom);
- const std::string& database_name =
- title_database.GetTitleName(m_gametdb_id, m_triforce_id, GetConfigLanguage());
+ const std::string& database_name = title_database.GetTitleName(m_gametdb_id, GetConfigLanguage());
return database_name.empty() ? GetName(Variant::LongAndPossiblyCustom) : database_name;
}
diff --git a/Source/Core/UICommon/GameFile.h b/Source/Core/UICommon/GameFile.h
index f015fa9725..e43950143b 100644
--- a/Source/Core/UICommon/GameFile.h
+++ b/Source/Core/UICommon/GameFile.h
@@ -76,7 +76,6 @@ public:
const std::string& GetInternalName() const { return m_internal_name; }
const std::string& GetGameID() const { return m_game_id; }
const std::string& GetGameTDBID() const { return m_gametdb_id; }
- std::string GetTriforceID() const { return m_triforce_id; }
u64 GetTitleID() const { return m_title_id; }
const std::string& GetMakerID() const { return m_maker_id; }
u16 GetRevision() const { return m_revision; }
@@ -160,7 +159,6 @@ private:
std::string m_internal_name;
std::string m_game_id;
std::string m_gametdb_id;
- std::string m_triforce_id;
u64 m_title_id{};
std::string m_maker_id;
diff --git a/Source/Core/UICommon/GameFileCache.cpp b/Source/Core/UICommon/GameFileCache.cpp
index 37a130599d..898e818852 100644
--- a/Source/Core/UICommon/GameFileCache.cpp
+++ b/Source/Core/UICommon/GameFileCache.cpp
@@ -23,7 +23,7 @@
namespace UICommon
{
-static constexpr u32 CACHE_REVISION = 26; // Last changed in PR 10084
+static constexpr u32 CACHE_REVISION = 27; // Last changed in PR 13844
std::vector<std::string> FindAllGamePaths(std::span<const std::string_view> directories_to_scan,
bool recursive_scan)
diff --git a/Source/Core/UICommon/UICommon.cpp b/Source/Core/UICommon/UICommon.cpp
index 43973a02e0..c56d1e1d09 100644
--- a/Source/Core/UICommon/UICommon.cpp
+++ b/Source/Core/UICommon/UICommon.cpp
@@ -280,6 +280,7 @@ void CreateDirectories()
File::CreateFullPath(File::GetUserPath(D_GCUSER_IDX) + USA_DIR DIR_SEP);
File::CreateFullPath(File::GetUserPath(D_GCUSER_IDX) + EUR_DIR DIR_SEP);
File::CreateFullPath(File::GetUserPath(D_GCUSER_IDX) + JAP_DIR DIR_SEP);
+ File::CreateFullPath(File::GetUserPath(D_TRIUSER_IDX));
File::CreateFullPath(File::GetUserPath(D_HIRESTEXTURES_IDX));
File::CreateFullPath(File::GetUserPath(D_GRAPHICSMOD_IDX));
File::CreateFullPath(File::GetUserPath(D_MAPS_IDX));
diff --git a/Source/UnitTests/Common/StringUtilTest.cpp b/Source/UnitTests/Common/StringUtilTest.cpp
index 59ba99a436..f7c3b9169b 100644
--- a/Source/UnitTests/Common/StringUtilTest.cpp
+++ b/Source/UnitTests/Common/StringUtilTest.cpp
@@ -6,6 +6,7 @@
#include <string>
#include <vector>
+#include "Common/Network.h"
#include "Common/StringUtil.h"
#include "Common/Swap.h"
@@ -259,6 +260,155 @@ TEST(StringUtil, CaseInsensitiveContains_OverlappingMatches)
EXPECT_TRUE(Common::CaseInsensitiveContains("ababababa", "bABa"));
}
+TEST(StringUtil, SplitStringIntoArray)
+{
+ constexpr auto subject0 = "";
+ EXPECT_FALSE(SplitStringIntoArray<2>(subject0, '.').has_value());
+
+ constexpr auto subject1 = "hello";
+ EXPECT_FALSE(SplitStringIntoArray<2>(subject1, '.').has_value());
+
+ constexpr auto subject2 = "hello.world";
+ const auto split2 = SplitStringIntoArray<2>(subject2, '.');
+ EXPECT_TRUE(split2.has_value() && split2->at(1) == "world");
+ EXPECT_FALSE(SplitStringIntoArray<3>(subject2, '.').has_value());
+
+ constexpr auto subject3 = "hello.universe.";
+ EXPECT_FALSE(SplitStringIntoArray<2>(subject3, '.').has_value());
+ const auto split3 = SplitStringIntoArray<3>(subject3, '.');
+ EXPECT_TRUE(split3.has_value() && split3->at(2) == "");
+ EXPECT_FALSE(SplitStringIntoArray<4>(subject3, '.').has_value());
+
+ constexpr auto subject4 = "=";
+ EXPECT_TRUE(SplitStringIntoArray<2>(subject4, '=').has_value());
+}
+
+TEST(StringUtil, StringToIPv4PortRange)
+{
+ constexpr auto parse = Common::StringToIPv4PortRange;
+
+ constexpr Common::IPAddress ip_network = {192, 168, 0, 0};
+ constexpr Common::IPAddress test_ip = {192, 168, 0, 13};
+ constexpr Common::IPAddress last_ip = {192, 168, 0, 255};
+
+ EXPECT_FALSE(parse("").has_value());
+ EXPECT_FALSE(parse("1.2.3").has_value());
+ EXPECT_FALSE(parse("1.2.3.4-").has_value());
+ EXPECT_FALSE(parse("1.2.3.4.5").has_value());
+ EXPECT_FALSE(parse("1.2.3.256").has_value());
+ EXPECT_FALSE(parse("1.2.3.4/33").has_value());
+ EXPECT_FALSE(parse("1.2.3.4:65536").has_value());
+ EXPECT_FALSE(parse("1.2.3.4-1.2.3.4.5").has_value());
+ EXPECT_FALSE(parse("1.1.1.1:81-80").has_value());
+ EXPECT_FALSE(parse("1.1.1.2-1.1.1.1").has_value());
+
+ EXPECT_TRUE(parse("1.2.3.4").has_value());
+ EXPECT_TRUE(parse("1.2.3.4:80").has_value());
+ EXPECT_TRUE(parse("1.2.3.4:80-81").has_value());
+ EXPECT_TRUE(parse("1.2.3.4/16").has_value());
+ EXPECT_TRUE(parse("1.2.3.4/16:80").has_value());
+ EXPECT_TRUE(parse("1.2.3.4/16:80-81").has_value());
+ EXPECT_TRUE(parse("1.2.3.4-10").has_value());
+ EXPECT_TRUE(parse("1.2.3.4-10:80").has_value());
+ EXPECT_TRUE(parse("1.2.3.4-10:80-81").has_value());
+
+ {
+ const auto simple_result = parse("192.168.0.13").value();
+ EXPECT_EQ(simple_result.first.ip_address, test_ip);
+ EXPECT_EQ(simple_result.last.ip_address, test_ip);
+ EXPECT_EQ(simple_result.first.GetPortValue(), 0);
+ EXPECT_EQ(simple_result.last.GetPortValue(), u16(-1));
+ }
+
+ {
+ const auto port_result = parse("192.168.0.13:77").value();
+ EXPECT_EQ(port_result.first.ip_address, test_ip);
+ EXPECT_EQ(port_result.last.ip_address, test_ip);
+ EXPECT_EQ(port_result.first.GetPortValue(), 77);
+ EXPECT_EQ(port_result.last.GetPortValue(), 77);
+ }
+
+ {
+ const auto ip_range_result = parse("192.168.0.13-192.168.0.255:123").value();
+ EXPECT_EQ(ip_range_result.first.ip_address, test_ip);
+ EXPECT_EQ(ip_range_result.last.ip_address, last_ip);
+ EXPECT_EQ(ip_range_result.first.GetPortValue(), 123);
+ EXPECT_EQ(ip_range_result.last.GetPortValue(), 123);
+ }
+
+ {
+ const auto port_range_result = parse("192.168.29.151:5000-5010").value();
+ EXPECT_EQ(port_range_result.first.GetPortValue(), 5000);
+ EXPECT_EQ(port_range_result.last.GetPortValue(), 5010);
+ EXPECT_TRUE(port_range_result.IsMatch(parse("192.168.29.151:5008")->first));
+ EXPECT_FALSE(port_range_result.IsMatch(parse("127.0.0.1:5000")->first));
+ }
+
+ {
+ const auto ip_range_result = parse("192.168.0.13-255:123-999").value();
+ EXPECT_EQ(ip_range_result.first.ip_address, test_ip);
+ EXPECT_EQ(ip_range_result.last.ip_address, last_ip);
+ EXPECT_EQ(ip_range_result.first.GetPortValue(), 123);
+ EXPECT_EQ(ip_range_result.last.GetPortValue(), 999);
+ }
+
+ {
+ const auto ip_network_result = parse("192.168.0.13/24:123-999").value();
+ EXPECT_EQ(ip_network_result.first.ip_address, ip_network);
+ EXPECT_EQ(ip_network_result.last.ip_address, last_ip);
+ EXPECT_EQ(ip_network_result.first.GetPortValue(), 123);
+ EXPECT_EQ(ip_network_result.last.GetPortValue(), 999);
+ }
+
+ EXPECT_TRUE(parse("192.168.0.13/24")->IsMatch(parse("192.168.0.99")->first));
+ EXPECT_FALSE(parse("192.168.0.13/24")->IsMatch(parse("192.168.1.99")->first));
+
+ EXPECT_FALSE(parse("192.168.0.13/32")->IsMatch(parse("192.168.0.99")->first));
+ EXPECT_TRUE(parse("192.168.0.13/0")->IsMatch(parse("10.0.0.1")->first));
+
+ EXPECT_TRUE(parse("192.168.0.13")->IsMatch(parse("192.168.0.13:81")->first));
+ EXPECT_FALSE(parse("192.168.0.13:80")->IsMatch(parse("192.168.0.13:81")->first));
+ EXPECT_TRUE(parse("192.168.0.13:80")->IsMatch(parse("192.168.0.13:80")->first));
+ EXPECT_FALSE(parse("192.168.0.13:80-81")->IsMatch(parse("192.168.0.14:81")->first));
+ EXPECT_TRUE(parse("192.168.0.13-14:80-81")->IsMatch(parse("192.168.0.14:81")->first));
+}
+
+TEST(StringUtil, IPv4PortRangeToString)
+{
+ Common::IPv4PortRange subject{
+ .first = {{10, 3, 0, 127}, 0},
+ .last = {{10, 3, 0, 127}, 0},
+ };
+
+ EXPECT_EQ(subject.ToString(), "10.3.0.127");
+
+ subject.last.port = Common::swap16(80); // First port is still zero.
+ EXPECT_EQ(subject.ToString(), "10.3.0.127");
+
+ subject.first.port = Common::swap16(80);
+ EXPECT_EQ(subject.ToString(), "10.3.0.127:80");
+
+ subject.last.port = Common::swap16(88);
+ EXPECT_EQ(subject.ToString(), "10.3.0.127:80-88");
+
+ subject.last.ip_address = {10, 3, 0, 128};
+ EXPECT_EQ(subject.ToString(), "10.3.0.127-128:80-88");
+
+ subject.last.ip_address = {10, 3, 7, 35};
+ EXPECT_EQ(subject.ToString(), "10.3.0.127-10.3.7.35:80-88");
+
+ subject.first.ip_address = {192, 168, 0, 0};
+ subject.last.ip_address = {192, 168, 0, 255};
+ EXPECT_EQ(subject.ToString(), "192.168.0.0/24:80-88");
+
+ subject.last.ip_address = {192, 168, 3, 255};
+ EXPECT_EQ(subject.ToString(), "192.168.0.0/22:80-88");
+
+ subject.first.ip_address = {};
+ subject.last.ip_address = {255, 255, 255, 255};
+ EXPECT_EQ(subject.ToString(), "0.0.0.0/0:80-88");
+}
+
TEST(StringUtil, CharacterEncodingConversion)
{
// wstring