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/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/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 714ec84f9f..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 @@ -1276,7 +1276,17 @@ class SettingsFragmentPresenter( } private fun addSerialPortSubSettings(sl: ArrayList, 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, 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 @@ @string/broadband_adapter_hle @string/broadband_adapter_tapserver @string/modem_adapter_tapserver + @string/sp1_am_baseboard 255 @@ -113,6 +114,7 @@ 12 11 13 + 6 @@ -375,6 +377,7 @@ @string/gcpad_steering_wheel @string/gcpad_dance_mat @string/gcpad_taru_konga + @string/gcpad_am_baseboard @string/gcpad_gc_adapter @@ -384,6 +387,7 @@ 8 9 10 + 11 12 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 @@ Enter the socket path or netloc (address:port) of the tapserver instance DNS Server Use 8.8.8.8 for normal DNS, else enter your custom one + IP Redirections Interface @@ -707,6 +708,7 @@ It can efficiently compress both junk data and encrypted Wii data. Broadband Adapter (HLE) Broadband Adapter (tapserver) Modem Adapter (tapserver) + Triforce Baseboard Mono @@ -811,6 +813,7 @@ It can efficiently compress both junk data and encrypted Wii data. Steering Wheel Dance Mat DK Bongos + Triforce Baseboard GameCube Adapter 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 } template -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(), "SetBit is only sane on unsigned types."); @@ -200,7 +200,7 @@ void SetBit(T& value, size_t bit_number, bool bit_value) } template -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/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 #ifndef _WIN32 +#include #include -#include +#include #else #include #endif #include +#include #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>(ip_address); +} + +u16 IPv4Port::GetPortValue() const +{ + return std::bit_cast>(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 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 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(result->first.ip_address) & net_mask; + result->first.ip_address = std::bit_cast(first_ip_address_u32); + result->last.ip_address = std::bit_cast(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(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(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 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 #include +#if defined(__linux__) || defined(__HAIKU__) +#include +#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 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 #include #include +#include #include #include #include @@ -182,6 +183,32 @@ std::from_chars_result FromChars(std::string_view sv, std::floating_point auto& std::vector SplitString(const std::string& str, char delim); +// Returns `nullopt` if subject does not contain exactly (Count - 1) delim. +template +requires(Count > 1) +std::optional> SplitStringIntoArray(std::string_view subject, + char delim) +{ + std::optional> 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 + +#if defined(__linux__) #include +#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/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 c5a526d262..85444ef0b6 100644 --- a/Source/Core/Core/BootManager.cpp +++ b/Source/Core/Core/BootManager.cpp @@ -25,6 +25,7 @@ #include "Core/AchievementManager.h" #include "Core/Boot/Boot.h" +#include "Core/Config/GraphicsSettings.h" #include "Core/Config/MainSettings.h" #include "Core/ConfigLoaders/BaseConfigLoader.h" #include "Core/ConfigLoaders/NetPlayConfigLoader.h" @@ -117,7 +118,7 @@ bool BootCore(Core::System& system, std::unique_ptr boot, case DiscIO::Region::NTSC_K: Config::SetCurrent(Config::SYSCONF_COUNTRY, 0x88); // South Korea break; - case DiscIO::Region::Unknown: + default: break; } } @@ -133,6 +134,28 @@ bool BootCore(Core::System& system, std::unique_ptr 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); 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/Config/MainSettings.cpp b/Source/Core/Core/Config/MainSettings.cpp index d11d48c898..6d5f260f07 100644 --- a/Source/Core/Core/Config/MainSettings.cpp +++ b/Source/Core/Core/Config/MainSettings.cpp @@ -6,6 +6,7 @@ #include #include +#include #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" @@ -639,6 +643,45 @@ const std::array, EMULATED_LOGITECH_MIC_COUNT> MAIN_LOGITECH_MIC_VOLUM Info{{System::Main, "EmulatedUSBDevices", "LogitechMic3VolumeModifier"}, 0}, Info{{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 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) @@ -672,6 +715,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; @@ -725,6 +771,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 path_region = std::nullopt; if (name.ends_with(us_region)) { @@ -741,6 +788,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)); @@ -784,6 +836,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 path_region = std::nullopt; if (base_path.ends_with(us_region)) @@ -801,6 +854,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 00c11ea6dd..38c8fdc365 100644 --- a/Source/Core/Core/Config/MainSettings.h +++ b/Source/Core/Core/Config/MainSettings.h @@ -386,6 +386,8 @@ extern const std::array, EMULATED_LOGITECH_MIC_COUNT> extern const std::array, EMULATED_LOGITECH_MIC_COUNT> MAIN_LOGITECH_MIC_MUTED; extern const std::array, EMULATED_LOGITECH_MIC_COUNT> MAIN_LOGITECH_MIC_VOLUME_MODIFIER; +extern const Info 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/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 lock(m_metadata_lock); - return m_triforce_id; -} - u64 SConfig::GetTitleID() const { std::lock_guard lock(m_metadata_lock); @@ -163,7 +157,7 @@ u16 SConfig::GetRevision() const void SConfig::ResetRunningGameMetadata() { std::lock_guard 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 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 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 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/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 +#include +#include +#include +#include + +#include + +#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 + +#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 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 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 padding; +}; +static_assert(sizeof(GuestSocketAddress) == 16); + +static bool s_firmware_map = false; +static bool s_test_menu = false; +static std::array 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 s_dimm_disc; + +static std::array s_firmware; +static std::array s_media_buffer_32; +static u8* const s_media_buffer = reinterpret_cast(s_media_buffer_32.data()); +static std::array s_network_command_buffer; +static std::array s_network_buffer; +static std::array s_allnet_buffer; +static std::array 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®ion0=jap®ion_name0=japan®ion_name1=usa®ion_" + "name2=asia®ion_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 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 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 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 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(max_length, buffer.data() + buffer.size() - ptr); + const auto length = strnlen(reinterpret_cast(ptr), adjusted_length); + + return {reinterpret_cast(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(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 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 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::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(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 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 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 port_distribution{first_port_value, last_port_value}; + + sockaddr_in addr = { + .sin_family = AF_INET, + .sin_addr = std::bit_cast(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(&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(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(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(&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::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(&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(&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(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(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(Common::BitCastPtr(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(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(adjusted_ipv4port.ip_address), + }; + + const int bind_result = bind(host_socket, reinterpret_cast(&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(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(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(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(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(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 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 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(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::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 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(s_media_buffer_32[parameter_offset + 1]); + const int optname = static_cast(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(s_media_buffer_32[parameter_offset + 4]); + + if (!NetworkCMDBufferCheck(optval_offset, optlen)) + { + return; + } + + const char* optval = reinterpret_cast(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& 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(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(s_media_buffer + 0x202); + + DEBUG_LOG_FMT(AMMEDIABOARD, "GC-AM: Execute command: (2){0:04X}", + static_cast(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(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(&timeout_b), sizeof(int)); + if (ret < 0) + { + ret = WSAGetLastError(); + } + else + { + ret = setsockopt(host_socket, SOL_SOCKET, SO_RCVTIMEO, + reinterpret_cast(&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(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(s_media_buffer + 0x22); + + DEBUG_LOG_FMT(AMMEDIABOARD, "GC-AM: Execute command: (1):{0:04X}", + static_cast(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(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(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(s_media_buffer_32[12]); + const auto addrb = std::bit_cast(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 data_u32 = Common::BitCastPtr>(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(ammb_command)); + break; + } + + memset(s_media_buffer + 0x20, 0, 0x20); + return 0; + } + + PanicAlertFmtT("Unhandled Media Board Execute:{0:04x}", static_cast(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 +#include +#include +#include + +#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 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& 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 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; + +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 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(&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_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 +#include + +#include + +#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(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 data(backup_size); + m_backup.ReadBytes(data.data(), data.size()); + + // Set FIRM version + Common::BitCastPtr(data.data() + 0x12) = 0x1703; + Common::BitCastPtr(data.data() + 0x212) = 0x1703; + + // Update checksum + Common::BitCastPtr(data.data() + 0x0A) = Common::swap16(CheckSum(&data[0xC], 0x1F4)); + Common::BitCastPtr(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; + } + + if ((m_position >= 2) && (m_command[0] == 0 && m_command[1] == 0)) + { + // 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) + { + 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]) + { + 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(SP1, "Unknown AM-BB command: {:02x}", m_command[0]); + break; + } } else { - switch (m_command) - { - case 0x00: - { - static constexpr std::array ID = {0x06, 0x04, 0x10, 0x00}; - byte = ID[(m_position - 2) & 3]; - break; - } - default: - ERROR_LOG_FMT(EXPANSIONINTERFACE, "EXI BASEBOARD: Unhandled command {:#x} {:#x}", m_command, - m_position); - } + 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 - u32 m_position = 0; - u32 m_command = 0; + 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_backup_dma_offset = 0; + u32 m_backup_dma_length = 0; + std::array 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(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 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 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 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..e84ef48a98 --- /dev/null +++ b/Source/Core/Core/HW/MagCard/MagneticCardReader.cpp @@ -0,0 +1,1002 @@ +// 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 +#include + +#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 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: + if (!IsCardPresent()) + { + m_status.s = S::WAITING_FOR_CARD; + --m_current_step; + break; + } + break; + case 3: + MoveCard(R::THERMAL_HEAD); + break; + case 4: + EjectCard(); + break; + default: + break; + } + + if (m_current_step >= 4) + 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(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* read, std::vector* 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 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 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& 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 +#include +#include +#include +#include + +#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* read, std::vector* 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 packet); + void BuildPacket(std::vector& 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 m_command_payload; + + // The card track data in memory. + using TrackData = std::optional>; + std::array m_card_data; + + // The parameter data of the command being processed. + std::vector 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 SIDevice_Create(Core::System& system, const SIDevices return std::make_unique(system, device, port_number); case SIDEVICE_AM_BASEBOARD: + return std::make_unique(system, device, port_number); + case SIDEVICE_NONE: default: return std::make_unique(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 +#include +#include + +#include + +#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(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(&m_mag_card_settings); + break; + + case MarioKartGP: + case MarioKartGP2: + m_mag_card_reader = std::make_unique(&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(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(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 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(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(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(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 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{} == 2 && + std::tuple_size{} == 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(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 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 m_coin{}; + std::array 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 m_mag_card_in_buffer; + std::vector m_mag_card_out_buffer; + + std::unique_ptr 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/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& 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(numPAD); + return NetPlay::netplay_client->InGamePadToLocalPad(pad_num); - return numPAD; + 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(pad_num); + + return pad_num; } 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 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((title_id >> 24) & 0xff), static_cast((title_id >> 16) & 0xff), static_cast((title_id >> 8) & 0xff), static_cast(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 m_cache_filler; @@ -228,11 +230,17 @@ private: std::unique_ptr CreateCachedBlobReader(std::unique_ptr reader) { + if (reader->IsCached()) // This is already CachedBlobReader. + return reader; + return std::make_unique(std::move(reader), false); } std::unique_ptr CreateScrubbingCachedBlobReader(std::unique_ptr reader) { + if (reader->IsCached()) // This reader is already a CachedBlobReader. + return reader; + return std::make_unique(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 #include #include +#include #include #include @@ -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 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 GetSyncHash() const = 0; protected: - template - std::string DecodeString(const char (&data)[N]) const + std::string DecodeString(std::span 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(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(maker_id.size(), 2)); + + return DecodeString(id); + } if (!Read(0, sizeof(id), reinterpret_cast(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(3, partition).value_or(0); + u8 country_byte = 0; + + if (GetVolumeType() == Platform::Triforce) + { + const BootID* boot_id = static_cast(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(3, partition).value_or(0); + } + const Region region = GetRegion(); const std::optional 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(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(&maker_id), partition)) return std::string(); @@ -66,6 +150,14 @@ std::optional 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(this)->GetTriforceBootID(); + return DecodeString(boot_id->game_name); + } + if (!Read(0x20, sizeof(name), reinterpret_cast(&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 #include #include @@ -15,6 +16,30 @@ namespace DiscIO class VolumeDisc : public Volume { public: + struct BootID + { + u32 magic; // 0x00 (Always BTID) + std::array unknown0; + std::array unknown1; + std::array 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 game_id; // 0x30 + uint32_t region_flags; // 0x38 + std::array unknown6; + std::array manufacturer; // 0x60 + std::array game_name; // 0x80 + std::array game_executable; // 0xA0 + std::array test_executable; // 0xC0 + std::array, 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 reader) std::unique_ptr 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(&triforce_header), sizeof(BootID)); - if (file_size >= 4 && triforce_header.magic == BTID_MAGIC) + reinterpret_cast(&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 -{ - if (m_is_triforce) - return (std::string(m_triforce_id.data(), m_triforce_id.size())); - else - return ""; -} - Region VolumeGC::GetRegion() const { + if (m_is_triforce) + { + 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; + } + } + return RegionCodeToRegion(m_reader->ReadSwapped(0x458)); } @@ -184,6 +189,12 @@ std::array 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(&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 GetShortNames() const override; std::map GetLongNames() const override; std::map GetShortMakers() const override; @@ -50,9 +49,10 @@ public: DataSizeType GetDataSizeType() const override; u64 GetRawSize() const override; const BlobReader& GetBlobReader() const override; - std::array 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 id; - }; - struct ConvertedGCBanner { ConvertedGCBanner(); @@ -112,8 +105,9 @@ private: std::unique_ptr m_reader; + BootID m_triforce_header; + bool m_is_triforce; - std::array 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 @@ + @@ -316,12 +317,16 @@ + + + + @@ -971,6 +976,7 @@ + @@ -1012,11 +1018,15 @@ + + + + diff --git a/Source/Core/DolphinQt/CMakeLists.txt b/Source/Core/DolphinQt/CMakeLists.txt index 457ed8a157..c61edc244b 100644 --- a/Source/Core/DolphinQt/CMakeLists.txt +++ b/Source/Core/DolphinQt/CMakeLists.txt @@ -376,6 +376,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/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 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/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 #endif +#include + #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(&sock_addr), &sock_addr_len) != 0) + return new QTableWidgetItem(); + const Common::IPv4Port sock_ip_port{std::bit_cast(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(&peer_addr), &peer_addr_len) == 0; + const Common::IPv4Port peer_ip_port = + has_peer ? Common::IPv4Port{std::bit_cast(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(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 09f9163036..9df5caf413 100644 --- a/Source/Core/DolphinQt/DolphinQt.vcxproj +++ b/Source/Core/DolphinQt/DolphinQt.vcxproj @@ -223,6 +223,7 @@ + @@ -439,6 +440,7 @@ + @@ -523,4 +525,4 @@ - \ No newline at end of file + 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&& parameters) if (!NKitWarningDialog::ShowUnlessDisabled()) return; } + + const auto volume_type = + std::get(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/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 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 +#include +#include +#include +#include +#include + +#include +#include + +#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 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 + +class TriforcePane final : public QWidget +{ + Q_OBJECT + +public: + TriforcePane(); +}; 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 FindAllGamePaths(std::span 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 #include +#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