Merge pull request #13844 from crediar/master

Core: Triforce support
This commit is contained in:
JosJuice authored and GitHub committed 2026-02-16 20:17:21 +01:00
commit 23f5d096ae
96 files changed
+8323 -285

No files matched your search

+4
View File
@@ -0,0 +1,4 @@
# GEYJ6E - Virtua Striker 2002 (Japan)
[Core]
FPRF = True
+4
View File
@@ -0,0 +1,4 @@
# GEYP6E - Virtua Striker 2002 (Export, Type3)
[Core]
FPRF = True
+5
View File
@@ -0,0 +1,5 @@
# GGGE6E - F-ZERO AX
[Core]
FPRF = True
+5
View File
@@ -0,0 +1,5 @@
# GHAE6E - F-ZERO AX (Monster)
[Core]
FPRF = True
+4
View File
@@ -0,0 +1,4 @@
# GHZW6E - VIRTUA STRIKER 4 (Asia)
[Core]
FPRF = True
+4
View File
@@ -0,0 +1,4 @@
# GJAP6E - VIRTUA STRIKER 4 (Export)
[Core]
FPRF = True
+4
View File
@@ -0,0 +1,4 @@
# GLKJ6E - VIRTUA STRIKER 4 Ver.2006 (Japan)
[Core]
FPRF = True
+4
View File
@@ -0,0 +1,4 @@
# GLLP6E - VIRTUA STRIKER 4 Ver.2006 (Export)
[Core]
FPRF = True
+2
View File
@@ -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/):
@@ -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,
@@ -1276,7 +1276,17 @@ class SettingsFragmentPresenter(
}
private fun addSerialPortSubSettings(sl: ArrayList<SettingsItem>, serialPort1Type: Int) {
if (serialPort1Type == 10) {
if (serialPort1Type == 6) {
// Triforce Baseboard
sl.add(
InputStringSetting(
context,
StringSetting.MAIN_TRIFORCE_IP_REDIRECTIONS,
R.string.triforce_ip_redirections,
0
)
)
} else if (serialPort1Type == 10) {
// Broadband Adapter (XLink Kai)
sl.add(HyperLinkHeaderSetting(context, R.string.xlink_kai_guide_header, 0))
sl.add(
@@ -2352,7 +2362,7 @@ class SettingsFragmentPresenter(
private fun addGcPadSubSettings(sl: ArrayList<SettingsItem>, gcPadNumber: Int, gcPadType: Int) {
when (gcPadType) {
6, 8, 9, 10 -> {
6, 8, 9, 10, 11 -> {
// Emulated
val gcPad = EmulatedController.getGcPad(gcPadNumber)
@@ -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
@@ -105,6 +105,7 @@
<item>@string/broadband_adapter_hle</item>
<item>@string/broadband_adapter_tapserver</item>
<item>@string/modem_adapter_tapserver</item>
<item>@string/sp1_am_baseboard</item>
</string-array>
<integer-array name="serialPort1DeviceValues">
<item>255</item>
@@ -113,6 +114,7 @@
<item>12</item>
<item>11</item>
<item>13</item>
<item>6</item>
</integer-array>
<!-- Wii System Languages -->
@@ -375,6 +377,7 @@
<item>@string/gcpad_steering_wheel</item>
<item>@string/gcpad_dance_mat</item>
<item>@string/gcpad_taru_konga</item>
<item>@string/gcpad_am_baseboard</item>
<item>@string/gcpad_gc_adapter</item>
</string-array>
<integer-array name="gcpadTypeValues">
@@ -384,6 +387,7 @@
<item>8</item>
<item>9</item>
<item>10</item>
<item>11</item>
<item>12</item>
</integer-array>
@@ -142,6 +142,7 @@
<string name="modem_tapserver_destination_description">Enter the socket path or netloc (address:port) of the tapserver instance</string>
<string name="bba_builtin_dns">DNS Server</string>
<string name="bba_builtin_dns_description">Use 8.8.8.8 for normal DNS, else enter your custom one</string>
<string name="triforce_ip_redirections">IP Redirections</string>
<!-- Interface Preference Fragment -->
<string name="interface_submenu">Interface</string>
@@ -707,6 +708,7 @@ It can efficiently compress both junk data and encrypted Wii data.
<string name="broadband_adapter_hle">Broadband Adapter (HLE)</string>
<string name="broadband_adapter_tapserver">Broadband Adapter (tapserver)</string>
<string name="modem_adapter_tapserver">Modem Adapter (tapserver)</string>
<string name="sp1_am_baseboard">Triforce Baseboard</string>
<!-- Sound Mode -->
<string name="sound_mode_mono">Mono</string>
@@ -811,6 +813,7 @@ It can efficiently compress both junk data and encrypted Wii data.
<string name="gcpad_steering_wheel">Steering Wheel</string>
<string name="gcpad_dance_mat">Dance Mat</string>
<string name="gcpad_taru_konga">DK Bongos</string>
<string name="gcpad_am_baseboard">Triforce Baseboard</string>
<string name="gcpad_gc_adapter">GameCube Adapter</string>
<!-- Wiimote Types -->
+2 -2
View File
@@ -189,7 +189,7 @@ template <typename ValueType, typename From>
}
template <typename T>
void SetBit(T& value, size_t bit_number, bool bit_value)
constexpr void SetBit(T& value, size_t bit_number, bool bit_value)
{
static_assert(std::is_unsigned<T>(), "SetBit is only sane on unsigned types.");
@@ -200,7 +200,7 @@ void SetBit(T& value, size_t bit_number, bool bit_value)
}
template <size_t bit_number, typename T>
void SetBit(T& value, bool bit_value)
constexpr void SetBit(T& value, bool bit_value)
{
SetBit(value, bit_number, bit_value);
}
+3
View File
@@ -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"
+1
View File
@@ -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;
+2 -1
View File
@@ -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
+5
View File
@@ -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,
@@ -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"};
+228 -1
View File
@@ -9,21 +9,234 @@
#include <vector>
#ifndef _WIN32
#include <cstring>
#include <netinet/in.h>
#include <string.h>
#include <sys/socket.h>
#else
#include <WinSock2.h>
#endif
#include <fmt/format.h>
#include <fmt/ranges.h>
#include "Common/BitUtils.h"
#include "Common/CommonFuncs.h"
#include "Common/Logging/Log.h"
#include "Common/Random.h"
#include "Common/StringUtil.h"
#include "Common/Swap.h"
namespace Common
{
static constexpr auto GetMaskFromNetworkPrefixLength(u32 network_prefix_length)
{
return (network_prefix_length == 0) ?
u32(0) :
u32(BigEndianValue(u32(-1) >> (32 - network_prefix_length)));
}
u32 IPv4Port::GetIPAddressValue() const
{
return std::bit_cast<Common::BigEndianValue<u32>>(ip_address);
}
u16 IPv4Port::GetPortValue() const
{
return std::bit_cast<Common::BigEndianValue<u16>>(port);
}
bool IPv4PortRange::IsMatch(IPv4Port subject) const
{
const u32 ip_u32 = subject.GetIPAddressValue();
const u16 port_u16 = subject.GetPortValue();
return ip_u32 >= first.GetIPAddressValue() && ip_u32 <= last.GetIPAddressValue() &&
port_u16 >= first.GetPortValue() && port_u16 <= last.GetPortValue();
}
std::string IPv4PortRange::ToString() const
{
const u32 first_ip_value = first.GetIPAddressValue();
const u32 last_ip_value = last.GetIPAddressValue();
const u32 different_bits = first_ip_value ^ last_ip_value;
const u32 common_high_bits = std::countl_zero(different_bits);
std::string ip_range = Common::IPAddressToString(first.ip_address);
if (common_high_bits == 32)
{
// Identical first and last IP.
}
else if ((last_ip_value - first_ip_value + 1) << common_high_bits == 0)
{
// An exact network range can use CIDR notation.
ip_range = fmt::format("{}/{}", ip_range, common_high_bits);
}
else if (common_high_bits >= 24)
{
// Only the last octet is different.
ip_range = fmt::format("{}-{}", ip_range, last.ip_address.back());
}
else
{
// Plainly specified range.
ip_range = fmt::format("{}-{}", ip_range, Common::IPAddressToString(last.ip_address));
}
if (first.port == 0)
return ip_range;
if (first.port == last.port)
return fmt::format("{}:{}", ip_range, first.GetPortValue());
return fmt::format("{}:{}-{}", ip_range, first.GetPortValue(), last.GetPortValue());
}
std::string IPAddressToString(IPAddress ip_address)
{
return fmt::format("{}", fmt::join(ip_address, "."));
}
std::optional<IPv4PortRange> StringToIPv4PortRange(std::string_view subject)
{
// TODO: This could be optimized and improved a lot.
auto read_pos = std::ranges::find_first_of(subject, std::string_view{"-/:"});
const auto first_ip_octets = SplitStringIntoArray<4>({subject.begin(), read_pos}, '.');
if (!first_ip_octets.has_value())
return std::nullopt; // Wrong number of octets.
std::optional<IPv4PortRange> result;
result.emplace();
for (u32 i = 0; i != result->first.ip_address.size(); ++i)
{
if (!TryParse(std::string((*first_ip_octets)[i]), &result->first.ip_address[i]))
return std::nullopt; // Bad octet.
}
if (read_pos == subject.end())
{
// Last IP and ports not specified.
result->last.ip_address = result->first.ip_address;
result->first.port = 0;
result->last.port = u16(-1);
return result;
}
if (*read_pos == '-')
{
// Read last IP.
++read_pos;
const auto last_ip_end = std::ranges::find(read_pos, subject.end(), ':');
const auto last_ip_octets = SplitString({read_pos, last_ip_end}, '.');
// Copy octets from first_ip when less than 4 are specified.
// FYI: This currently allows for syntax like: 10.0.0.1-7.255
// I don't know if it's desirable or not to allow 2 or 3 octets on the last IP.
const std::size_t octet_share_count = 4 - last_ip_octets.size();
if (octet_share_count >= 4)
return std::nullopt; // Too many octets.
std::copy_n(result->first.ip_address.begin(), octet_share_count,
result->last.ip_address.begin());
std::size_t i = octet_share_count;
for (auto&& octet_str : last_ip_octets)
{
if (!TryParse(octet_str, &result->last.ip_address[i]))
return std::nullopt; // Bad octet.
++i;
}
if (result->first.ip_address > result->last.ip_address)
return std::nullopt; // Reversed range.
read_pos = last_ip_end;
}
else if (*read_pos == '/')
{
// Read network prefix length.
++read_pos;
u32 network_prefix_length{};
const auto [parse_end, ec] = FromChars({read_pos, subject.end()}, network_prefix_length);
if (ec != std::errc{} || network_prefix_length > 32)
return std::nullopt; // Bad network prefix.
read_pos += parse_end - std::to_address(read_pos); // Working around MSVC iterators..
const auto net_mask = GetMaskFromNetworkPrefixLength(network_prefix_length);
const auto first_ip_address_u32 = std::bit_cast<u32>(result->first.ip_address) & net_mask;
result->first.ip_address = std::bit_cast<IPAddress>(first_ip_address_u32);
result->last.ip_address = std::bit_cast<IPAddress>(first_ip_address_u32 | ~net_mask);
}
else
{
// Last IP not specified.
result->last.ip_address = result->first.ip_address;
}
if (read_pos == subject.end())
{
// Ports not specified.
result->first.port = 0;
result->last.port = u16(-1);
return result;
}
if (*read_pos != ':')
return std::nullopt; // Unexpected character.
u16 first_port_u16{};
{
// Read first port.
++read_pos;
const auto [parse_end, ec] = FromChars({read_pos, subject.end()}, first_port_u16);
if (ec != std::errc{})
return std::nullopt; // Bad first port.
result->first.port = std::bit_cast<u16>(Common::BigEndianValue(first_port_u16));
read_pos += parse_end - std::to_address(read_pos); // Working around MSVC iterators..
}
if (read_pos == subject.end())
{
// Last port not specified.
result->last.port = result->first.port;
return result;
}
if (*read_pos != '-')
return std::nullopt; // Unexpected character.
u16 first_last_u16{};
{
// Read last port.
++read_pos;
const auto [parse_end, ec] = FromChars({read_pos, subject.end()}, first_last_u16);
if (ec != std::errc{} || first_last_u16 < first_port_u16)
return std::nullopt; // Bad last port.
result->last.port = std::bit_cast<u16>(Common::BigEndianValue(first_last_u16));
read_pos += parse_end - std::to_address(read_pos); // Working around MSVC iterators..
}
if (read_pos != subject.end())
return std::nullopt;
return result;
}
MACAddress GenerateMacAddress(const MACConsumer type)
{
constexpr std::array<u8, 3> oui_bba{{0x00, 0x09, 0xbf}};
@@ -586,4 +799,18 @@ const char* StrNetworkError()
#endif
return DecodeNetworkError(error_code);
}
bool SetPlatformSocketOptions(int fd [[maybe_unused]])
{
#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
int opt_no_sigpipe = 1;
if (setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &opt_no_sigpipe, sizeof(opt_no_sigpipe)) < 0)
{
ERROR_LOG_FMT(COMMON, "Failed to set SO_NOSIGPIPE on socket: {}", StrNetworkError());
return false;
}
#endif
return true;
}
} // namespace Common
+39
View File
@@ -10,6 +10,10 @@
#include <type_traits>
#include <vector>
#if defined(__linux__) || defined(__HAIKU__)
#include <sys/socket.h>
#endif
#include "Common/CommonTypes.h"
struct sockaddr_in;
@@ -264,6 +268,30 @@ struct NetworkErrorState
#endif
};
struct IPv4Port
{
IPAddress ip_address;
u16 port; // Network byte order.
// These convert to host byte order.
u32 GetIPAddressValue() const;
u16 GetPortValue() const;
};
struct IPv4PortRange
{
IPv4Port first;
IPv4Port last;
bool IsMatch(IPv4Port subject) const;
std::string ToString() const;
};
std::string IPAddressToString(IPAddress ip_address);
// Syntax is: first_ip[-last_ip|/network_prefix_length][:first_port[-last_port]]
std::optional<IPv4PortRange> StringToIPv4PortRange(std::string_view subject);
MACAddress GenerateMacAddress(MACConsumer type);
std::string MacAddressToString(const MACAddress& mac);
@@ -279,4 +307,15 @@ NetworkErrorState SaveNetworkErrorState();
void RestoreNetworkErrorState(const NetworkErrorState& state);
const char* DecodeNetworkError(s32 error_code);
const char* StrNetworkError();
// Sets SO_NOSIGPIPE when available.
bool SetPlatformSocketOptions(int fd);
// Pass this to all `send` calls to avoid SIGPIPE.
#if defined(__linux__) || defined(__HAIKU__)
static constexpr int SEND_FLAGS = MSG_NOSIGNAL;
#else
static constexpr int SEND_FLAGS = 0;
#endif
} // namespace Common
+27
View File
@@ -12,6 +12,7 @@
#include <iomanip>
#include <limits>
#include <locale>
#include <ranges>
#include <span>
#include <sstream>
#include <string>
@@ -182,6 +183,32 @@ std::from_chars_result FromChars(std::string_view sv, std::floating_point auto&
std::vector<std::string> SplitString(const std::string& str, char delim);
// Returns `nullopt` if subject does not contain exactly (Count - 1) delim.
template <std::size_t Count>
requires(Count > 1)
std::optional<std::array<std::string_view, Count>> SplitStringIntoArray(std::string_view subject,
char delim)
{
std::optional<std::array<std::string_view, Count>> result;
result.emplace();
std::size_t index = 0;
for (auto&& item : subject | std::views::split(delim))
{
// Too many delim.
if (index == Count)
return std::nullopt;
(*result)[index++] = std::string_view{item};
}
// Too few delim.
if (index != Count)
return std::nullopt;
return result;
}
// "C:/Windows/winhelp.exe" to "C:/Windows/", "winhelp", ".exe"
// This requires forward slashes to be used for the path separators, even on Windows.
bool SplitPath(std::string_view full_path, std::string* path, std::string* filename,
+6
View File
@@ -4,13 +4,18 @@
#pragma once
#include <pthread.h>
#if defined(__linux__)
#include <sys/eventfd.h>
#endif
#include "Common/CommonFuncs.h"
#include "Common/Logging/Log.h"
namespace UnixUtil
{
#if defined(__linux__)
inline int CreateEventFD(unsigned int count, int flags)
{
const int result = eventfd(count, flags);
@@ -21,6 +26,7 @@ inline int CreateEventFD(unsigned int count, int flags)
}
return result;
}
#endif
// Repeatedly call a function that can erroneously produce EINTR.
auto RetryOnEINTR(auto func, auto... args)
+18 -1
View File
@@ -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)));
+8 -3
View File
@@ -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;
+24 -1
View File
@@ -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<BootParameters> 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<BootParameters> boot,
if (!boot->riivolution_patches.empty())
Config::SetCurrent(Config::MAIN_FAST_DISC_SPEED, true);
if (system.IsTriforce())
{
// Attach Triforce Baseboard hardware if not overridden in any layer.
// Users may set these in their GameConfig if they want them not automatically attached.
if (GetActiveLayerForConfig(Config::MAIN_SERIAL_PORT_1) == Config::LayerType::Base)
{
Config::SetCurrent(Config::MAIN_SERIAL_PORT_1, ExpansionInterface::EXIDeviceType::Baseboard);
}
if (GetActiveLayerForConfig(Config::GetInfoForSIDevice(0)) == Config::LayerType::Base)
{
Config::SetCurrent(Config::GetInfoForSIDevice(0),
SerialInterface::SIDevices::SIDEVICE_AM_BASEBOARD);
}
// Mario Kart Arcade GP has widescreen heuristic issues.
// All Triforce games are 4:3 so we'll just disable the heuristic for now.
if (GetActiveLayerForConfig(Config::GFX_SUGGESTED_ASPECT_RATIO) == Config::LayerType::Base)
{
Config::SetCurrent(Config::GFX_SUGGESTED_ASPECT_RATIO, AspectMode::ForceStandard);
}
}
system.Initialize();
Core::UpdateWantDeterminism(system, /*initial*/ true);
+10
View File
@@ -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
+58
View File
@@ -6,6 +6,7 @@
#include <sstream>
#include <fmt/format.h>
#include <fmt/ranges.h>
#include "AudioCommon/AudioCommon.h"
#include "Common/Assert.h"
@@ -20,13 +21,16 @@
#include "Common/Version.h"
#include "Core/AchievementManager.h"
#include "Core/Config/DefaultLocale.h"
#include "Core/Core.h"
#include "Core/HW/EXI/EXI.h"
#include "Core/HW/EXI/EXI_Device.h"
#include "Core/HW/GCMemcard/GCMemcard.h"
#include "Core/HW/HSP/HSP_Device.h"
#include "Core/HW/Memmap.h"
#include "Core/HW/SI/SI_Device.h"
#include "Core/IOS/Network/Socket.h"
#include "Core/PowerPC/PowerPC.h"
#include "Core/System.h"
#include "Core/USBUtils.h"
#include "DiscIO/Enums.h"
#include "VideoCommon/VideoBackendBase.h"
@@ -639,6 +643,45 @@ const std::array<Info<s16>, EMULATED_LOGITECH_MIC_COUNT> MAIN_LOGITECH_MIC_VOLUM
Info<s16>{{System::Main, "EmulatedUSBDevices", "LogitechMic3VolumeModifier"}, 0},
Info<s16>{{System::Main, "EmulatedUSBDevices", "LogitechMic4VolumeModifier"}, 0}};
static std::string GetDefaultTriforceIPRedirections()
{
#if defined(ANDROID)
return "0.0.0.0/0=127.0.0.1";
#else
constexpr std::string_view entries[] = {
// Mario Kart Arcade GP 1 + 2
// This config allows for 4x Multicabinet on the same PC.
"192.168.29.150:5000-5008=127.0.0.1:5000-5008 MarioKart Seat #1 Static",
"192.168.29.151:5000-5008=127.0.0.1:5010-5018 MarioKart Seat #2 Static",
"192.168.29.152:5000-5008=127.0.0.1:5020-5028 MarioKart Seat #3 Static",
"192.168.29.153:5000-5008=127.0.0.1:5030-5038 MarioKart Seat #4 Static",
// Ephemeral ports are constrained to differentiate incoming connections.
"192.168.29.150=127.0.0.1:50000-50999 MarioKart Seat #1 Ephemeral",
"192.168.29.151=127.0.0.1:51000-51999 MarioKart Seat #2 Ephemeral",
"192.168.29.152=127.0.0.1:52000-52999 MarioKart Seat #3 Ephemeral",
"192.168.29.153=127.0.0.1:53000-53999 MarioKart Seat #4 Ephemeral",
// The cameras.
"192.168.29.104-107=127.0.0.1 MarioKart namcam2",
// CyCraft Connect IP
"192.168.11.0/24=127.0.0.1 CyCraft",
// The Key of Avalon
// This config isn't usable as-is. It's just here for reference.
"192.168.13.0/24=127.0.0.1 The Key of Avalon",
// Sega ALL.Net
"192.168.150.0/24=127.0.0.1 ALL.Net",
};
return fmt::format("{}", fmt::join(entries, ","));
#endif
}
const Info<std::string> MAIN_TRIFORCE_IP_REDIRECTIONS{
{System::Main, "Core", "TriforceIPRedirections"}, GetDefaultTriforceIPRedirections()};
// The reason we need this function is because some memory card code
// expects to get a non-NTSC-K region even if we're emulating an NTSC-K Wii.
DiscIO::Region ToGameCubeRegion(DiscIO::Region region)
@@ -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<DiscIO::Region> 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<DiscIO::Region> 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));
+2
View File
@@ -386,6 +386,8 @@ extern const std::array<Info<std::string>, EMULATED_LOGITECH_MIC_COUNT>
extern const std::array<Info<bool>, EMULATED_LOGITECH_MIC_COUNT> MAIN_LOGITECH_MIC_MUTED;
extern const std::array<Info<s16>, EMULATED_LOGITECH_MIC_COUNT> MAIN_LOGITECH_MIC_VOLUME_MODIFIER;
extern const Info<std::string> MAIN_TRIFORCE_IP_REDIRECTIONS;
// GameCube path utility functions
// Replaces NTSC-K with some other region, and doesn't replace non-NTSC-K regions
+12 -18
View File
@@ -142,12 +142,6 @@ const std::string SConfig::GetTitleDescription() const
return m_title_description;
}
std::string SConfig::GetTriforceID() const
{
std::lock_guard<std::recursive_mutex> lock(m_metadata_lock);
return m_triforce_id;
}
u64 SConfig::GetTitleID() const
{
std::lock_guard<std::recursive_mutex> lock(m_metadata_lock);
@@ -163,7 +157,7 @@ u16 SConfig::GetRevision() const
void SConfig::ResetRunningGameMetadata()
{
std::lock_guard<std::recursive_mutex> lock(m_metadata_lock);
SetRunningGameMetadata("00000000", "", "", 0, 0, DiscIO::Region::Unknown);
SetRunningGameMetadata("00000000", "", 0, 0, DiscIO::Region::Unknown);
}
void SConfig::SetRunningGameMetadata(const DiscIO::Volume& volume,
@@ -172,14 +166,14 @@ void SConfig::SetRunningGameMetadata(const DiscIO::Volume& volume,
std::lock_guard<std::recursive_mutex> lock(m_metadata_lock);
if (partition == volume.GetGamePartition())
{
SetRunningGameMetadata(volume.GetGameID(), volume.GetGameTDBID(), volume.GetTriforceID(),
SetRunningGameMetadata(volume.GetGameID(), volume.GetGameTDBID(),
volume.GetTitleID().value_or(0), volume.GetRevision().value_or(0),
volume.GetRegion());
}
else
{
SetRunningGameMetadata(volume.GetGameID(partition), volume.GetGameTDBID(partition),
volume.GetTriforceID(), volume.GetTitleID(partition).value_or(0),
volume.GetTitleID(partition).value_or(0),
volume.GetRevision(partition).value_or(0), volume.GetRegion());
}
}
@@ -197,28 +191,25 @@ void SConfig::SetRunningGameMetadata(const IOS::ES::TMDReader& tmd, DiscIO::Plat
!Core::System::GetInstance().GetDVDInterface().UpdateRunningGameMetadata(tmd_title_id))
{
// If not launching a disc game, just read everything from the TMD.
SetRunningGameMetadata(tmd.GetGameID(), tmd.GetGameTDBID(), "", tmd_title_id,
tmd.GetTitleVersion(), tmd.GetRegion());
SetRunningGameMetadata(tmd.GetGameID(), tmd.GetGameTDBID(), tmd_title_id, tmd.GetTitleVersion(),
tmd.GetRegion());
}
}
void SConfig::SetRunningGameMetadata(const std::string& game_id)
{
std::lock_guard<std::recursive_mutex> lock(m_metadata_lock);
SetRunningGameMetadata(game_id, "", "", 0, 0, DiscIO::Region::Unknown);
SetRunningGameMetadata(game_id, "", 0, 0, DiscIO::Region::Unknown);
}
void SConfig::SetRunningGameMetadata(const std::string& game_id, const std::string& gametdb_id,
std::string triforce_id, u64 title_id, u16 revision,
DiscIO::Region region)
u64 title_id, u16 revision, DiscIO::Region region)
{
std::lock_guard<std::recursive_mutex> lock(m_metadata_lock);
const bool was_changed = m_game_id != game_id || m_gametdb_id != gametdb_id ||
m_triforce_id != triforce_id || m_title_id != title_id ||
m_revision != revision;
m_title_id != title_id || m_revision != revision;
m_game_id = game_id;
m_gametdb_id = gametdb_id;
m_triforce_id = triforce_id;
m_title_id = title_id;
m_revision = revision;
@@ -251,7 +242,7 @@ void SConfig::SetRunningGameMetadata(const std::string& game_id, const std::stri
const Core::TitleDatabase title_database;
auto& system = Core::System::GetInstance();
const DiscIO::Language language = GetLanguageAdjustedForRegion(system.IsWii(), region);
m_title_name = title_database.GetTitleName(m_gametdb_id, m_triforce_id, language);
m_title_name = title_database.GetTitleName(m_gametdb_id, language);
m_title_description = title_database.Describe(m_gametdb_id, language);
NOTICE_LOG_FMT(CORE, "Active title: {}", m_title_description);
Host_TitleChanged();
@@ -458,6 +449,9 @@ bool SConfig::SetPathsAndGameMetadata(Core::System& system, const BootParameters
if (m_region == DiscIO::Region::Unknown)
m_region = Config::Get(Config::MAIN_FALLBACK_REGION);
if (system.IsTriforce())
m_region = DiscIO::Region::DEV;
// Set up paths
const std::string region_dir = Config::GetDirectoryForRegion(Config::ToGameCubeRegion(m_region));
m_strSRAM = File::GetUserPath(F_GCSRAM_IDX);
+1 -5
View File
@@ -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;
File diff suppressed because it is too large. Load diff
+274
View File
@@ -0,0 +1,274 @@
// Copyright 2025 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <optional>
#include <span>
#include <string_view>
#include "Common/ChunkFile.h"
#include "Common/CommonTypes.h"
#include "Common/Network.h"
#include "DiscIO/Volume.h"
namespace Core
{
class CPUThreadGuard;
class System;
} // namespace Core
namespace AMMediaboard
{
enum GameType
{
FZeroAX = 1,
FZeroAXMonster,
MarioKartGP,
MarioKartGP2,
VirtuaStriker3,
VirtuaStriker4,
VirtuaStriker4_2006,
GekitouProYakyuu,
KeyOfAvalon,
FirmwareUpdate,
};
enum MediaType
{
GDROM = 1,
NAND,
};
enum MediaBoardType
{
NANDMaskBoardHDD = 0,
NANDMaskBoardMask = 1,
NANDMaskBoardNAND = 2,
DIMMBoardType3 = 4,
};
enum MediaBoardStatus
{
Initializing = 0,
CheckingNetwork = 1,
SystemDisc = 2,
TestingGameProgram = 3,
LoadingGameProgram = 4,
LoadedGameProgram = 5,
Error = 6,
};
enum InquiryType
{
Version1 = 0x21484100,
Version2 = 0x29484100,
};
struct MediaBoardRange
{
u32 start;
u32 end;
u8* buffer;
std::size_t buffer_size;
MediaBoardRange(u32 start_, u32 size_, std::span<u8> buffer_);
};
enum class AMMBDICommand : u16
{
Inquiry = 0x12,
Read = 0xA8,
Write = 0xAA,
Execute = 0xAB,
};
enum class AMMBCommand : u16
{
Unknown_000 = 0x000,
GetDIMMSize = 0x001,
GetMediaBoardStatus = 0x100,
GetSegaBootVersion = 0x101,
GetSystemFlags = 0x102,
GetMediaBoardSerial = 0x103,
Unknown_104 = 0x104,
NetworkReInit = 0x204,
TestHardware = 0x301,
// Network used by Mario Kart GPs
Accept = 0x401,
Bind = 0x402,
Closesocket = 0x403,
Connect = 0x404,
GetIPbyDNS = 0x405,
InetAddr = 0x406,
Ioctl = 0x407, // Unused
Listen = 0x408,
Recv = 0x409,
Send = 0x40A,
Socket = 0x40B,
Select = 0x40C,
Shutdown = 0x40D, // Unused
SetSockOpt = 0x40E,
GetSockOpt = 0x40F, // Unused
SetTimeOuts = 0x410,
GetLastError = 0x411,
RouteAdd = 0x412, // Unused
RouteDelete = 0x413, // Unused
GetParambyDHCPExec = 0x414,
ModifyMyIPaddr = 0x415,
Recvfrom = 0x416, // Unused
Sendto = 0x417, // Unused
RecvDimmImage = 0x418, // Unused
SendDimmImage = 0x419, // Unused
// Network used by F-Zero AX
InitLink = 0x601,
Unknown_605 = 0x605,
SetupLink = 0x606,
SearchDevices = 0x607,
Unknown_608 = 0x608,
Unknown_614 = 0x614,
// All.Net used by Virtua Striker 4 2006 (Japan)
AllNetInit = 0x700,
Unknown_701 = 0x701,
Unknown_702 = 0x702,
Unknown_703 = 0x703,
Unknown_704 = 0x704,
// NETDIMM Commands
Unknown_001 = 0x001,
GetNetworkFirmVersion = 0x101,
Unknown_103 = 0x103,
};
enum MediaBoardAddress : u32
{
MediaBoardStatus1 = 0x80000000,
MediaBoardStatus2 = 0x80000020,
MediaBoardStatus3 = 0x80000040,
FirmwareStatus1 = 0x80000120,
FirmwareStatus2 = 0x80000140,
BackupMemory = 0x000006A0,
DIMMMemory = 0x1F000000,
DIMMMemory2 = 0xFF000000,
DIMMExtraSettings = 0x1FFEFFE0,
AllNetSettings = 0x1EFF8000,
NetworkControl = 0xFFFF0000,
DIMMCommandVersion1 = 0x1F900000,
DIMMCommandVersion2 = 0x84000000,
DIMMCommandVersion2_2 = 0x89000000,
DIMMCommandExecute1 = 0x84000040,
DIMMCommandExecute2 = 0x88000000,
NetworkCommandAddress1 = 0x1F800200,
NetworkCommandAddress2 = 0x89040200,
NetworkBufferAddress1 = 0x1FA00000,
NetworkBufferAddress2 = 0x1FD00000,
NetworkBufferAddress3 = 0x89100000,
NetworkBufferAddress4 = 0x89180000,
NetworkBufferAddress5 = 0x1FB00000,
AllNetBuffer = 0x89010000,
FirmwareAddress = 0x84800000,
FirmwareMagicWrite1 = 0x00600000,
FirmwareMagicWrite2 = 0x00700000,
};
// Mario Kart GP2 has a complete list of them
// but in japanese
// They somewhat match WSA errors codes
enum SocketStatusCodes
{
SSC_E_4 = -4, // Failure (abnormal argument)
SSC_E_3 = -3, // Success (unsupported command)
SSC_E_2 =
-2, // Failure (failed to send, abnormal argument, or communication condition violation)
SSC_E_1 = -1, // Failure (error termination)
SSC_EINTR = 4, // An interrupt occurred before data reception was completed
SSC_EBADF = 9, // Invalid descriptor
SSC_E_11 = 11, // Send operation was blocked on a non-blocking mode socket
SSC_EACCES = 13, // The socket does not support broadcast addresses, but the destination address
// is a broadcast address
SSC_EFAULT =
14, // The name argument specifies a location other than an address used by the process.
SSC_E_23 = 23, // System file table is full.
SSC_AEMFILE = 24, // Process descriptor table is full.
SSC_EMSGSIZE =
36, // Socket tried to send message without splitting, but message size is too large.
SSC_EAFNOSUPPORT = 47, // Address prohibited for use on this socket.
SSC_EADDRINUSE = 48, // Address already in use.
SSC_EADDRNOTAVAIL = 49, // Prohibited address.
SSC_E_50 = 50, // Non-socket descriptor.
SSC_ENETUNREACH = 51, // Cannot access specified network.
SSC_ENOBUFS = 55, // Insufficient buffer
SSC_EISCONN = 56, // Already connected socket
SSC_ENOTCONN = 57, // No connection for connection-type socket
SSC_ETIMEDOUT = 60, // Timeout
SSC_ECONNREFUSED = 61, // Connection request forcibly rejected
SSC_EHOSTUNREACH = 65, // Remote host cannot be reached
SSC_EHOSTDOWN = 67, // Remote host is down
SSC_EWOULDBLOCK = 70, // Socket is in non-blocking mode and connection has not been completed
SSC_E_69 = 69, // Socket is in non-blocking mode and a previously issued Connect command has not
// been completed
SSC_SUCCESS = 70,
};
static constexpr std::size_t SOCKET_FD_MAX = 64;
void Init();
void FirmwareMap(bool on);
void InitDIMM(const DiscIO::Volume& volume);
void InitKeys(u32 key_a, u32 key_b, u32 key_c);
u32 ExecuteCommand(std::array<u32, 3>& dicmd_buf, u32* diimm_buf, u32 address, u32 length);
u32 GetGameType();
u32 GetMediaType();
bool GetTestMenu();
void Shutdown();
void DoState(PointerWrap& p);
struct ParsedIPRedirection
{
std::string_view original;
std::string_view replacement;
std::string_view description;
};
std::optional<ParsedIPRedirection> ParseIPRedirection(std::string_view str);
struct IPRedirection
{
Common::IPv4PortRange original;
Common::IPv4PortRange replacement;
// Caller should check if it matches first!
Common::IPv4Port Apply(Common::IPv4Port subject) const;
Common::IPv4Port Reverse(Common::IPv4Port subject) const;
std::string ToString() const;
};
using IPRedirections = std::vector<IPRedirection>;
IPRedirections GetIPRedirections();
s32 DebuggerGetSocket(u32 triforce_fd);
}; // namespace AMMediaboard
+45 -10
View File
@@ -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;
}
@@ -31,11 +31,7 @@ using ws_ssize_t = int;
using ws_ssize_t = ssize_t;
#endif
#ifdef __linux__
#define SEND_FLAGS MSG_NOSIGNAL
#else
#define SEND_FLAGS 0
#endif
using Common::SEND_FLAGS;
TAPServerConnection::TAPServerConnection(const std::string& destination,
std::function<void(std::string&&)> recv_cb,
@@ -114,11 +110,7 @@ static int ConnectToDestination(const std::string& destination)
return -1;
}
#ifdef __APPLE__
int opt_no_sigpipe = 1;
if (setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &opt_no_sigpipe, sizeof(opt_no_sigpipe)) < 0)
INFO_LOG_FMT(SP1, "Failed to set SO_NOSIGPIPE on socket\n");
#endif
Common::SetPlatformSocketOptions(fd);
if (connect(fd, reinterpret_cast<sockaddr*>(&ss), ss_size) == -1)
{
+9 -15
View File
@@ -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));
-1
View File
@@ -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);
+273 -16
View File
@@ -1,20 +1,107 @@
// Copyright 2013 Dolphin Emulator Project
// Copyright 2025 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "Core/HW/EXI/EXI_DeviceBaseboard.h"
#include "Common/ChunkFile.h"
#include <numeric>
#include <string>
#include <fmt/format.h>
#include "Common/BitUtils.h"
#include "Common/Buffer.h"
#include "Common/CommonTypes.h"
#include "Common/FileUtil.h"
#include "Common/IOFile.h"
#include "Common/Logging/Log.h"
#include "Core/ConfigManager.h"
#include "Core/Core.h"
#include "Core/HW/DVD/AMMediaboard.h"
#include "Core/HW/EXI/EXI.h"
#include "Core/HW/Memmap.h"
#include "Core/System.h"
static bool s_interrupt_set = false;
static u32 s_irq_timer = 0;
static u32 s_irq_status = 0;
static u16 CheckSum(const u8* data, u32 length)
{
return std::accumulate(data, data + length, 0);
}
namespace ExpansionInterface
{
void GenerateInterrupt(int flag)
{
auto& system = Core::System::GetInstance();
s_interrupt_set = true;
s_irq_timer = 0;
s_irq_status = flag;
system.GetExpansionInterface().UpdateInterrupts();
}
CEXIBaseboard::CEXIBaseboard(Core::System& system) : IEXIDevice(system)
{
std::string backup_filename(fmt::format("{}tribackup_{}.bin", File::GetUserPath(D_TRIUSER_IDX),
SConfig::GetInstance().GetGameID()));
m_backup = File::IOFile(backup_filename, "rb+");
if (!m_backup.IsOpen())
{
m_backup = File::IOFile(backup_filename, "wb+");
}
// Some games share the same ID Client/Server
if (!m_backup.IsGood())
{
PanicAlertFmt("Failed to open {}\nFile might be in use.", backup_filename.c_str());
std::srand(static_cast<u32>(std::time(nullptr)));
backup_filename = fmt::format("{}tribackup_tmp_{}{}.bin", File::GetUserPath(D_TRIUSER_IDX),
rand(), SConfig::GetInstance().GetGameID());
m_backup = File::IOFile(backup_filename, "wb+");
}
// Virtua Striker 4 and Gekitou Pro Yakyuu need a higher FIRM version
// Which is read from the backup data?!
if (AMMediaboard::GetGameType() == AMMediaboard::VirtuaStriker4 ||
AMMediaboard::GetGameType() == AMMediaboard::GekitouProYakyuu)
{
const u64 backup_size = m_backup.GetSize();
if (backup_size >= 0x20C + 0x1F4)
{
Common::UniqueBuffer<u8> data(backup_size);
m_backup.ReadBytes(data.data(), data.size());
// Set FIRM version
Common::BitCastPtr<u16>(data.data() + 0x12) = 0x1703;
Common::BitCastPtr<u16>(data.data() + 0x212) = 0x1703;
// Update checksum
Common::BitCastPtr<u16>(data.data() + 0x0A) = Common::swap16(CheckSum(&data[0xC], 0x1F4));
Common::BitCastPtr<u16>(data.data() + 0x20A) = Common::swap16(CheckSum(&data[0x20C], 0x1F4));
m_backup.Seek(0, File::SeekOrigin::Begin);
m_backup.WriteBytes(data.data(), data.size());
m_backup.Flush();
}
}
}
CEXIBaseboard::~CEXIBaseboard()
{
}
void CEXIBaseboard::SetCS(int cs)
{
DEBUG_LOG_FMT(SP1, "AM-BB: ChipSelect={}", cs);
if (cs)
m_position = 0;
}
@@ -24,33 +111,203 @@ bool CEXIBaseboard::IsPresent() const
return true;
}
bool CEXIBaseboard::IsInterruptSet()
{
if (s_interrupt_set)
{
DEBUG_LOG_FMT(SP1, "AM-BB: IRQ");
if (++s_irq_timer > 12)
s_interrupt_set = false;
return true;
}
return false;
}
void CEXIBaseboard::DMAWrite(u32 addr, u32 size)
{
const auto& system = Core::System::GetInstance();
const auto& memory = system.GetMemory();
auto span = memory.GetSpanForAddress(addr);
if (span.size() < size)
{
ERROR_LOG_FMT(SP1, "AM-BB: Backup DMA Write overflow: address=0x{:08x}, length={}, span={}",
addr, size, span.size());
return;
}
NOTICE_LOG_FMT(SP1, "AM-BB: COMMAND: Backup DMA Write: {:08x} {:x}", addr, size);
m_backup.Seek(m_backup_offset, File::SeekOrigin::Begin);
m_backup.WriteBytes(span.data(), size);
m_backup.Flush();
}
void CEXIBaseboard::DMARead(u32 addr, u32 size)
{
const auto& system = Core::System::GetInstance();
const auto& memory = system.GetMemory();
auto span = memory.GetSpanForAddress(addr);
if (span.size() < size)
{
ERROR_LOG_FMT(SP1, "AM-BB: Backup DMA Read overflow: address=0x{:08x}, length={}, span={}",
addr, size, span.size());
return;
}
NOTICE_LOG_FMT(SP1, "AM-BB: COMMAND: Backup DMA Read: {:08x} {:x}", addr, size);
m_backup.Seek(m_backup_offset, File::SeekOrigin::Begin);
m_backup.ReadBytes(span.data(), size);
}
void CEXIBaseboard::TransferByte(u8& byte)
{
if (m_position == 0)
DEBUG_LOG_FMT(SP1, "AM-BB: > {:02x}", byte);
if (m_position < 4)
{
m_command = byte;
m_command[m_position] = byte;
byte = 0xFF;
}
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<u8, 4> 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
+39 -7
View File
@@ -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<u8, 4> m_command{};
u16 m_backup_offset = 0;
File::IOFile m_backup;
protected:
void TransferByte(u8& byte) override;
};
} // namespace ExpansionInterface
+21
View File
@@ -34,6 +34,7 @@ static const u16 trigger_bitmasks[] = {
static const u16 dpad_bitmasks[] = {PAD_BUTTON_UP, PAD_BUTTON_DOWN, PAD_BUTTON_LEFT,
PAD_BUTTON_RIGHT};
static const u8 triforce_bitmask[] = {SWITCH_TEST, SWITCH_SERVICE, SWITCH_COIN};
GCPad::GCPad(const unsigned int index) : m_index(index)
{
@@ -68,6 +69,13 @@ GCPad::GCPad(const unsigned int index) : m_index(index)
m_dpad->AddInput(Translatability::Translate, named_direction);
}
// triforce
groups.emplace_back(m_triforce = new ControllerEmu::Buttons(TRIFORCE_GROUP));
for (const char* named_button : {TEST_BUTTON, SERVICE_BUTTON, COIN_BUTTON})
{
m_triforce->AddInput(Translatability::Translate, named_button);
}
// Microphone
groups.emplace_back(m_mic = new ControllerEmu::Buttons(MIC_GROUP));
m_mic->AddInput(Translatability::Translate, _trans("Button"));
@@ -119,6 +127,8 @@ ControllerEmu::ControlGroup* GCPad::GetGroup(PadGroup group)
return m_mic;
case PadGroup::Options:
return m_options;
case PadGroup::Triforce:
return m_triforce;
default:
return nullptr;
}
@@ -150,6 +160,9 @@ GCPadStatus GCPad::GetInput() const
// dpad
m_dpad->GetState(&pad.button, dpad_bitmasks, m_input_override_function);
// triforce
m_triforce->GetState(&pad.switches, triforce_bitmask, m_input_override_function);
// sticks
const auto main_stick_state = m_main_stick->GetState(m_input_override_function);
pad.stickX = MapFloat<u8>(main_stick_state.x, GCPadStatus::MAIN_STICK_CENTER_X, 1);
@@ -181,6 +194,9 @@ void GCPad::LoadDefaults(const ControllerInterface& ciface)
#ifdef ANDROID
// Rumble
m_rumble->SetControlExpression(0, "`Android/0/Device Sensors:Motor 0`");
// Triforce Coin
m_triforce->SetControlExpression(2, "pulse(`Android/0/Device Sensors:Accel Down` > 15, 0.1)");
#else
// Buttons: A, B, X, Y, Z
m_buttons->SetControlExpression(0, "`X`");
@@ -202,6 +218,11 @@ void GCPad::LoadDefaults(const ControllerInterface& ciface)
m_dpad->SetControlExpression(2, "`F`"); // Left
m_dpad->SetControlExpression(3, "`H`"); // Right
// Triforce
m_triforce->SetControlExpression(0, "`1`"); // Test
m_triforce->SetControlExpression(1, "`2`"); // Service
m_triforce->SetControlExpression(2, "`3`"); // Coin
// C Stick
m_c_stick->SetControlExpression(0, "`I`"); // Up
m_c_stick->SetControlExpression(1, "`K`"); // Down
+9 -1
View File
@@ -29,7 +29,8 @@ enum class PadGroup
Triggers,
Rumble,
Mic,
Options
Options,
Triforce,
};
class GCPad : public ControllerEmu::EmulatedController
@@ -61,6 +62,7 @@ public:
static constexpr const char* RUMBLE_GROUP = _trans("Rumble");
static constexpr const char* MIC_GROUP = _trans("Microphone");
static constexpr const char* OPTIONS_GROUP = _trans("Options");
static constexpr const char* TRIFORCE_GROUP = _trans("Triforce");
static constexpr const char* A_BUTTON = "A";
static constexpr const char* B_BUTTON = "B";
@@ -69,6 +71,11 @@ public:
static constexpr const char* Z_BUTTON = "Z";
static constexpr const char* START_BUTTON = "Start";
// Special Triforce buttons
static constexpr const char* TEST_BUTTON = _trans("Test");
static constexpr const char* SERVICE_BUTTON = _trans("Service");
static constexpr const char* COIN_BUTTON = _trans("Coin");
// i18n: The left trigger button (labeled L on real controllers)
static constexpr const char* L_DIGITAL = _trans("L");
// i18n: The right trigger button (labeled R on real controllers)
@@ -87,6 +94,7 @@ private:
ControllerEmu::ControlGroup* m_rumble;
ControllerEmu::Buttons* m_mic;
ControllerEmu::ControlGroup* m_options;
ControllerEmu::Buttons* m_triforce;
ControllerEmu::SettingValue<bool> m_always_connected_setting;
+82
View File
@@ -0,0 +1,82 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// Based on: https://github.com/GXTX/YACardEmu
// Copyright (C) 2020-2023 wutno (https://github.com/GXTX)
// Copyright (C) 2022-2023 tugpoat (https://github.com/tugpoat)
// SPDX-License-Identifier: GPL-2.0-or-later
#include "Core/HW/MagCard/C1231BR.h"
namespace MagCard
{
void C1231BR::Command_10_Initialize()
{
// TODO: Not hardware tested.
if (!m_current_packet.empty())
{
if (InitMode(m_current_packet[0]) == InitMode::EjectAfter)
{
m_is_shutter_open = true;
MoveCard(R::ENTRY_SLOT);
}
}
// m_status.SoftReset(); // TODO: Needed ?
FinishCommand();
}
void C1231BR::Command_D0_ControlShutter()
{
enum class Action : u8
{
Close = 0x30,
Open = 0x31,
};
if (m_current_packet.empty())
{
SetPError(P::SYSTEM_ERR);
FinishCommand();
return;
}
m_is_shutter_open = Action(m_current_packet[0]) == Action::Open;
INFO_LOG_FMT(SERIALINTERFACE_CARD, "Shutter: {}", m_is_shutter_open ? "Open" : "Close");
FinishCommand();
}
u8 C1231BR::GetPositionValue()
{
// True "R" (Card Position) values.
// FYI: I see a 0xd0 command transition from: 11000 -> 01110 -> 00111
// Are these 5 bits individual sensors placed within the machine ?
static constexpr std::array<u8, std::size_t(R::MAX_POSITIONS)> POSITION_VALUES{
0b00000, // NO_CARD
0b11000, // READ_WRITE_HEAD
0b00111, // THERMAL_HEAD
0b11100, // DISPENSER_THERMAL
0b00001, // ENTRY_SLOT
};
return (m_is_shutter_open ? 0x80u : 0x40u) |
(m_card_settings->report_dispenser_empty ? 0x00u : 0x20u) |
POSITION_VALUES[u8(m_status.r)];
}
void C1231BR::DoState(PointerWrap& p)
{
MagneticCardReader::DoState(p);
p.Do(m_is_shutter_open);
}
bool C1231BR::IsReadyForCard()
{
return !IsCardPresent() && m_is_shutter_open;
}
} // namespace MagCard
+37
View File
@@ -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
+27
View File
@@ -0,0 +1,27 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// Based on: https://github.com/GXTX/YACardEmu
// Copyright (C) 2020-2023 wutno (https://github.com/GXTX)
// SPDX-License-Identifier: GPL-2.0-or-later
#include "Core/HW/MagCard/C1231LR.h"
namespace MagCard
{
u8 C1231LR::GetPositionValue()
{
// True "R" (Card Position) values.
static constexpr std::array<u8, std::size_t(R::MAX_POSITIONS)> POSITION_VALUES{
0x30, // NO_CARD
0x31, // READ_WRITE_HEAD
0x32, // THERMAL_HEAD
0x33, // DISPENSER_THERMAL
0x34, // ENTRY_SLOT
};
return POSITION_VALUES[u8(m_status.r)];
}
} // namespace MagCard
+26
View File
@@ -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
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,207 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// Based on: https://github.com/GXTX/YACardEmu
// Copyright (C) 2020-2023 wutno (https://github.com/GXTX)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <optional>
#include <span>
#include <string>
#include <vector>
#include "Common/ChunkFile.h"
#include "Common/CommonTypes.h"
namespace MagCard
{
class MagneticCardReader
{
public:
static constexpr std::size_t TRACK_SIZE = 69; // A nice amount of data.
static constexpr std::size_t NUM_TRACKS = 3;
// This data is not part of the card reader state.
struct Settings
{
std::string card_name;
std::string card_path;
bool insert_card_when_possible = true;
bool report_dispenser_empty = false;
};
explicit MagneticCardReader(Settings* settings);
virtual ~MagneticCardReader();
MagneticCardReader(const MagneticCardReader&) = delete;
MagneticCardReader& operator=(const MagneticCardReader&) = delete;
MagneticCardReader(MagneticCardReader&&) = delete;
MagneticCardReader& operator=(MagneticCardReader&&) = delete;
// TODO: This std:vector buffer interface is a bit funky..
// read = MagCard <-- Baseboard.
// write = Magcard --> Baseboard.
void Process(std::vector<u8>* read, std::vector<u8>* write);
virtual void DoState(PointerWrap& p);
protected:
// Status bytes:
enum class R : u8
{
// Note: Derived classes define the real values in GetPositionValue().
NO_CARD,
READ_WRITE_HEAD,
THERMAL_HEAD,
DISPENSER_THERMAL,
ENTRY_SLOT,
MAX_POSITIONS,
};
enum class P : u8
{
NO_ERR = 0x30,
READ_ERR = 0x31,
WRITE_ERR = 0x32,
CARD_JAM = 0x33,
MOTOR_ERR = 0x34, // transport system motor error
PRINT_ERR = 0x35,
ILLEGAL_ERR = 0x38, // generic error
BATTERY_ERR = 0x40, // low battery voltage
SYSTEM_ERR = 0x41, // reader/writer system err
TRACK_1_READ_ERR = 0x51,
TRACK_2_READ_ERR = 0x52,
TRACK_3_READ_ERR = 0x53,
TRACK_1_AND_2_READ_ERR = 0x54,
TRACK_1_AND_3_READ_ERR = 0x55,
TRACK_2_AND_3_READ_ERR = 0x56,
};
enum class S : u8
{
NO_JOB = 0x30,
ILLEGAL_COMMAND = 0x32,
RUNNING_COMMAND = 0x33,
WAITING_FOR_CARD = 0x34,
DISPENSER_EMPTY = 0x35,
NO_DISPENSER = 0x36,
CARD_FULL = 0x37,
};
struct Status
{
R r = R::NO_CARD;
P p = P::NO_ERR;
S s = S::NO_JOB;
void SoftReset()
{
p = P::NO_ERR;
s = S::NO_JOB;
}
};
enum class InitMode : u8
{
Standard = 0x30,
EjectAfter = 0x31,
ResetSpecifications = 0x32,
};
bool ReceivePacket(std::span<const u8> packet);
void BuildPacket(std::vector<u8>& write_buffer);
void StepStateMachine(DT elapsed_time);
void StepStatePerson(DT elapsed_time);
// This is what actually maps the "R" position value to a number.
virtual u8 GetPositionValue() = 0;
void SetPError(P error_code);
void SetSError(S error_code);
void ClearCardInMemory();
// Read/Write with host filesystem.
void ReadCardFile();
void WriteCardFile();
bool IsRunningCommand() const;
void FinishCommand();
u8 GetCurrentCommand() const;
// Is a card anywhere the machine can sense it ?
bool IsCardPresent() const;
// Is a card beyond the "ENTRY_SLOT" position ?
bool IsCardLoaded() const;
virtual bool IsReadyForCard();
// Move the card through the machine.
void MoveCard(R new_position);
// Unloads the card and moves it to the "ENTRY_SLOT" position.
void EjectCard();
// Feeds a new card into the machine from an internal hopper.
void DispenseCard();
// Commands
virtual void Command_10_Initialize();
void Command_20_ReadStatus();
void Command_33_ReadData(); // Multi-track read.
void Command_35_GetData(); // Read the entire card.
void Command_40_Cancel();
void Command_53_WriteData(); // Multi-track write.
void Command_78_PrintSettings();
void Command_7A_RegisterFont();
void Command_7B_PrintImage();
void Command_7C_PrintLine();
void Command_7D_Erase(); // Erase the printed image.
void Command_7E_PrintBarcode();
void Command_80_EjectCard();
void Command_A0_Clean();
void Command_B0_DispenseCard();
void Command_C0_ControlLED();
void Command_C1_SetPrintRetry();
virtual void Command_D0_ControlShutter();
void Command_F0_GetVersion();
Settings* const m_card_settings;
// R P S values that are sent in every ENQUIRY response.
Status m_status{};
// A byte sent in every ENQUIRY response.
// TODO: What's the default value on just powered up hardware ?
u8 m_current_command = 0;
// The data included in every ENQUIRY response.
std::vector<u8> m_command_payload;
// The card track data in memory.
using TrackData = std::optional<std::array<u8, TRACK_SIZE>>;
std::array<TrackData, NUM_TRACKS> m_card_data;
// The parameter data of the command being processed.
std::vector<u8> m_current_packet;
// The progress state of the current command.
// 0 == not running a command.
// TODO: Change this to emulated milliseconds or something.
s32 m_current_step = 0;
// Used to delay re-insertion of cards after ejection.
// This avoids having to expose card "Take"/"Insert" actions.
DT m_time_since_machine_moved_card{};
DT m_time_since_person_moved_card{};
};
} // namespace MagCard
-1
View File
@@ -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);
+1
View File
@@ -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);
+4
View File
@@ -17,6 +17,7 @@
#ifdef HAS_LIBMGBA
#include "Core/HW/SI/SI_DeviceGBAEmu.h"
#endif
#include "Core/HW/SI/SI_DeviceAMBaseboard.h"
#include "Core/HW/SI/SI_DeviceGCAdapter.h"
#include "Core/HW/SI/SI_DeviceGCController.h"
#include "Core/HW/SI/SI_DeviceGCSteeringWheel.h"
@@ -149,6 +150,7 @@ bool SIDevice_IsGCController(SIDevices type)
case SIDEVICE_GC_TARUKONGA:
case SIDEVICE_DANCEMAT:
case SIDEVICE_GC_STEERING:
case SIDEVICE_AM_BASEBOARD:
return true;
default:
return false;
@@ -191,6 +193,8 @@ std::unique_ptr<ISIDevice> SIDevice_Create(Core::System& system, const SIDevices
return std::make_unique<CSIDevice_Keyboard>(system, device, port_number);
case SIDEVICE_AM_BASEBOARD:
return std::make_unique<CSIDevice_AMBaseboard>(system, device, port_number);
case SIDEVICE_NONE:
default:
return std::make_unique<CSIDevice_Null>(system, device, port_number);
-2
View File
@@ -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,
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,315 @@
// Copyright 2025 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "Core/HW/GCPad.h"
#include "Core/HW/MagCard/MagneticCardReader.h"
#include "Core/HW/SI/SI_Device.h"
#include "InputCommon/GCPadStatus.h"
namespace Movie
{
class MovieManager;
}
namespace SerialInterface
{
// "JAMMA Video Standard" I/O
class JVSIOMessage
{
public:
void Start(int node);
void AddData(const u8* dst, std::size_t len, int sync);
void AddData(const void* data, std::size_t len);
void AddData(const char* data);
void AddData(u32 n);
void End();
u32 m_pointer = 0;
std::array<u8, 0x80> m_message;
private:
u32 m_last_start = 0;
u32 m_checksum = 0;
};
// Triforce (GC-AM) baseboard
class CSIDevice_AMBaseboard : public ISIDevice
{
enum EButtonCombo
{
COMBO_NONE = 0,
COMBO_ORIGIN,
COMBO_RESET
};
public:
// constructor
CSIDevice_AMBaseboard(Core::System& system, SIDevices device, int device_number);
// run the SI Buffer
int RunBuffer(u8* buffer, int request_length) override;
// Reply has to be delayed due a bug in the parser
void SwapBuffers(u8* buffer, u32* buffer_length);
// return true on new data
DataResponse GetData(u32& hi, u32& low) override;
// send a command directly
void SendCommand(u32 command, u8 poll) override;
virtual GCPadStatus GetPadStatus();
virtual u32 MapPadStatus(const GCPadStatus& pad_status);
virtual EButtonCombo HandleButtonCombos(const GCPadStatus& pad_status);
static void HandleMoviePadStatus(Movie::MovieManager& movie, int device_number,
GCPadStatus* pad_status);
// Send and Receive pad input from network
static bool NetPlay_GetInput(int pad_num, GCPadStatus* status);
static int NetPlay_InGamePadToLocalPad(int pad_num);
void DoState(PointerWrap&) override;
protected:
struct SOrigin
{
u16 button;
u8 origin_stick_x;
u8 origin_stick_y;
u8 substick_x;
u8 substick_y;
u8 trigger_left;
u8 trigger_right;
u8 unk_4;
u8 unk_5;
};
// struct to compare input against
// Set on connection to perfect neutral values
// (standard pad only) Set on button combo to current input state
SOrigin m_origin = {};
// PADAnalogMode
// Dunno if we need to do this, game/lib should set it?
u8 m_mode = 0x3;
// Timer to track special button combos:
// y, X, start for 3 seconds updates origin with current status
// Technically, the above is only on standard pad, wavebird does not support it for example
// b, x, start for 3 seconds triggers reset (PI reset button interrupt)
u64 m_timer_button_combo_start = 0;
// Type of button combo from the last/current poll
EButtonCombo m_last_button_combo = COMBO_NONE;
void SetOrigin(const GCPadStatus& pad_status);
private:
enum BaseBoardCommand
{
GCAM_Reset = 0x00,
GCAM_Command = 0x70,
};
enum GCAMCommand
{
StatusSwitches = 0x10,
SerialNumber = 0x11,
Unknown_12 = 0x12,
Unknown_14 = 0x14,
FirmVersion = 0x15,
FPGAVersion = 0x16,
RegionSettings = 0x1F,
Unknown_21 = 0x21,
Unknown_22 = 0x22,
Unknown_23 = 0x23,
Unknown_24 = 0x24,
SerialA = 0x31,
SerialB = 0x32,
JVSIOA = 0x40,
JVSIOB = 0x41,
Unknown_60 = 0x60,
};
enum JVSIOCommand
{
IOID = 0x10,
CommandRevision = 0x11,
JVRevision = 0x12,
CommunicationVersion = 0x13,
CheckFunctionality = 0x14,
MainID = 0x15,
SwitchesInput = 0x20,
CoinInput = 0x21,
AnalogInput = 0x22,
RotaryInput = 0x23,
KeyCodeInput = 0x24,
PositionInput = 0x25,
GeneralSwitchInput = 0x26,
PayoutRemain = 0x2E,
Retrans = 0x2F,
CoinSubOutput = 0x30,
PayoutAddOutput = 0x31,
GeneralDriverOutput = 0x32,
AnalogOutput = 0x33,
CharacterOutput = 0x34,
CoinAddOutput = 0x35,
PayoutSubOutput = 0x36,
GeneralDriverOutput2 = 0x37,
GeneralDriverOutput3 = 0x38,
NAMCOCommand = 0x70,
Reset = 0xF0,
SetAddress = 0xF1,
ChangeComm = 0xF2,
};
enum JVSIOStatusCode
{
StatusOkay = 1,
UnsupportedCommand = 2,
ChecksumError = 3,
AcknowledgeOverflow = 4,
};
enum CARDCommand
{
Init = 0x10,
GetState = 0x20,
Read = 0x33,
IsPresent = 0x40,
Write = 0x53,
SetPrintParam = 0x78,
RegisterFont = 0x7A,
WriteInfo = 0x7C,
Erase = 0x7D,
Eject = 0x80,
Clean = 0xA0,
Load = 0xB0,
SetShutter = 0xD0,
};
enum ICCARDCommand
{
GetStatus = 0x10,
SetBaudrate = 0x11,
FieldOn = 0x14,
FieldOff = 0x15,
InsertCheck = 0x20,
AntiCollision = 0x21,
SelectCard = 0x22,
ReadPage = 0x24,
WritePage = 0x25,
DecreaseUseCount = 0x26,
ReadUseCount = 0x33,
ReadPages = 0x34,
WritePages = 0x35,
};
enum ICCARDStatus
{
Okay = 0,
NoCard = 0x8000,
Unknown = 0x800E,
BadCard = 0xFFFF,
};
enum CDReaderCommand
{
ShutterAuto = 0x61,
BootVersion = 0x62,
SensLock = 0x63,
SensCard = 0x65,
FirmwareUpdate = 0x66,
ShutterGet = 0x67,
CameraCheck = 0x68,
ShutterCard = 0x69,
ProgramChecksum = 0x6B,
BootChecksum = 0x6D,
ShutterLoad = 0x6F,
ReadCard = 0x72,
ShutterSave = 0x73,
SelfTest = 0x74,
ProgramVersion = 0x76,
};
// NOTE: Used to be an union with `u8 data[81 + 4 + 4 + 4]`
// TODO: Should the struct be packed?
struct ICCommand
{
u32 pktcmd : 8;
u32 pktlen : 8;
u32 fixed : 8;
u32 command : 8;
u32 flag : 8;
u32 length : 8;
u32 status : 16;
u8 extdata[81] = {};
u32 extlen;
};
u8 m_last[2][0x80] = {};
u32 m_lastptr[2] = {};
std::array<u16, 2> m_coin{};
std::array<u32, 2> m_coin_pressed{};
u8 m_ic_card_data[2048] = {};
// Setup IC-card
u16 m_ic_card_state = 0x20;
u16 m_ic_card_status = ICCARDStatus::Okay;
u16 m_ic_card_session = 0x23;
u8 m_ic_write_buffer[512] = {};
u32 m_ic_write_offset = 0;
u32 m_ic_write_size = 0;
// Magnetic Card Reader
MagCard::MagneticCardReader::Settings m_mag_card_settings;
std::vector<u8> m_mag_card_in_buffer;
std::vector<u8> m_mag_card_out_buffer;
std::unique_ptr<MagCard::MagneticCardReader> m_mag_card_reader;
// Serial
u32 m_wheel_init = 0;
u32 m_motor_init = 0;
u8 m_motor_reply[64] = {};
s16 m_motor_force_y = 0;
// F-Zero AX (DX)
bool m_fzdx_seatbelt = true;
bool m_fzdx_motion_stop = false;
bool m_fzdx_sensor_right = false;
bool m_fzdx_sensor_left = false;
u8 m_rx_reply = 0xF0;
// F-Zero AX (CyCraft)
bool m_fzcc_seatbelt = true;
bool m_fzcc_sensor = false;
bool m_fzcc_emergency = false;
bool m_fzcc_service = false;
u32 m_dip_switch_1 = 0xFE;
u32 m_dip_switch_0 = 0xFF;
int m_delay = 0;
void ICCardSendReply(ICCommand* iccommand, u8* buffer, u32* length);
};
} // namespace SerialInterface
+3 -1
View File
@@ -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;
+1 -5
View File
@@ -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)
+27 -6
View File
@@ -52,6 +52,7 @@
#include "Core/HW/GCPad.h"
#include "Core/HW/SI/SI.h"
#include "Core/HW/SI/SI_Device.h"
#include "Core/HW/SI/SI_DeviceAMBaseboard.h"
#include "Core/HW/SI/SI_DeviceGCController.h"
#include "Core/HW/Sram.h"
#include "Core/HW/WiiSave.h"
@@ -1883,6 +1884,8 @@ void NetPlayClient::UpdateDevices()
auto& si = Core::System::GetInstance().GetSerialInterface();
for (auto player_id : m_pad_map)
{
const SerialInterface::SIDevices si_device = Config::Get(Config::GetInfoForSIDevice(local_pad));
if (m_gba_config[pad].enabled && player_id > 0)
{
si.ChangeDevice(SerialInterface::SIDEVICE_GC_GBA_EMULATED, pad);
@@ -1890,8 +1893,6 @@ void NetPlayClient::UpdateDevices()
else if (player_id == m_local_player->pid)
{
// Use local controller types for local controllers if they are compatible
const SerialInterface::SIDevices si_device =
Config::Get(Config::GetInfoForSIDevice(local_pad));
if (SerialInterface::SIDevice_IsGCController(si_device))
{
si.ChangeDevice(si_device, pad);
@@ -1909,7 +1910,8 @@ void NetPlayClient::UpdateDevices()
}
else if (player_id > 0)
{
si.ChangeDevice(SerialInterface::SIDEVICE_GC_CONTROLLER, pad);
if (si_device != SerialInterface::SIDEVICE_AM_BASEBOARD)
si.ChangeDevice(SerialInterface::SIDEVICE_GC_CONTROLLER, pad);
}
else
{
@@ -2772,6 +2774,16 @@ bool SerialInterface::CSIDevice_GCController::NetPlay_GetInput(int pad_num, GCPa
return false;
}
bool SerialInterface::CSIDevice_AMBaseboard::NetPlay_GetInput(int pad_num, GCPadStatus* status)
{
std::lock_guard lk(NetPlay::crit_netplay_client);
if (NetPlay::netplay_client)
return NetPlay::netplay_client->GetNetPads(pad_num, NetPlay::s_si_poll_batching, status);
return false;
}
bool NetPlay::NetPlay_GetWiimoteData(const std::span<NetPlayClient::WiimoteDataBatchEntry>& entries)
{
std::lock_guard lk(crit_netplay_client);
@@ -2835,12 +2847,21 @@ u64 ExpansionInterface::CEXIIPL::NetPlay_GetEmulatedTime()
// called from ---CPU--- thread
// return the local pad num that should rumble given a ingame pad num
int SerialInterface::CSIDevice_GCController::NetPlay_InGamePadToLocalPad(int numPAD)
int SerialInterface::CSIDevice_GCController::NetPlay_InGamePadToLocalPad(int pad_num)
{
std::lock_guard lk(NetPlay::crit_netplay_client);
if (NetPlay::netplay_client)
return NetPlay::netplay_client->InGamePadToLocalPad(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;
}
+1 -1
View File
@@ -95,7 +95,7 @@ struct CompressAndDumpStateArgs
static Common::WorkQueueThreadSP<CompressAndDumpStateArgs> s_compress_and_dump_thread;
// Don't forget to increase this after doing changes on the savestate system
constexpr u32 STATE_VERSION = 176; // Last changed in PR 13768
constexpr u32 STATE_VERSION = 177; // Last changed in PR 13844
// Increase this if the StateExtendedHeader definition changes
constexpr u32 EXTENDED_HEADER_VERSION = 1; // Last changed in PR 12217
+2 -26
View File
@@ -91,32 +91,8 @@ TitleDatabase::TitleDatabase()
TitleDatabase::~TitleDatabase() = default;
const std::string& TitleDatabase::GetTitleName(const std::string& gametdb_id,
const std::string& triforce_id,
DiscIO::Language language) const
{
if (triforce_id != "")
{
const Map& map = *m_triforce_title_maps.at(DiscIO::Language::English);
auto it = map.find(triforce_id);
if (it != map.end())
return it->second;
// This code has been commented out as there is currently only a english title map, and all
// Triforce games are detected as Japanese.
// if (language != DiscIO::Language::English)
//{
// const Map& english_triforce_map = *m_triforce_title_maps.at(DiscIO::Language::English);
// it = english_triforce_map.find(triforce_id);
// if (it != english_triforce_map.end())
// return it->second;
//}
// it = m_base_map.find(triforce_id);
// if (it != m_base_map.end())
// return it->second;
}
auto it = m_user_title_map.find(gametdb_id);
if (it != m_user_title_map.end())
return it->second;
@@ -149,12 +125,12 @@ const std::string& TitleDatabase::GetChannelName(u64 title_id, DiscIO::Language
const std::string id{
{static_cast<char>((title_id >> 24) & 0xff), static_cast<char>((title_id >> 16) & 0xff),
static_cast<char>((title_id >> 8) & 0xff), static_cast<char>(title_id & 0xff)}};
return GetTitleName(id, "", language);
return GetTitleName(id, language);
}
std::string TitleDatabase::Describe(const std::string& gametdb_id, DiscIO::Language language) const
{
const std::string& title_name = GetTitleName(gametdb_id, "", language);
const std::string& title_name = GetTitleName(gametdb_id, language);
if (title_name.empty())
return gametdb_id;
return fmt::format("{} ({})", title_name, gametdb_id);
+1 -2
View File
@@ -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;
+3
View File
@@ -97,6 +97,9 @@ public:
return false;
}
// Returns true only for CachedBlobReader.
virtual bool IsCached() const { return false; }
protected:
BlobReader() {}
};
+8
View File
@@ -219,6 +219,8 @@ public:
return m_cache_filler->Read(offset, size, out_ptr) || m_reader->Read(offset, size, out_ptr);
}
bool IsCached() const override { return true; }
private:
// A shared object does the cache filling for sensible CopyReader behavior.
const std::shared_ptr<CacheFiller> m_cache_filler;
@@ -228,11 +230,17 @@ private:
std::unique_ptr<BlobReader> CreateCachedBlobReader(std::unique_ptr<BlobReader> reader)
{
if (reader->IsCached()) // This is already CachedBlobReader.
return reader;
return std::make_unique<CachedBlobReader>(std::move(reader), false);
}
std::unique_ptr<BlobReader> CreateScrubbingCachedBlobReader(std::unique_ptr<BlobReader> reader)
{
if (reader->IsCached()) // This reader is already a CachedBlobReader.
return reader;
return std::make_unique<CachedBlobReader>(std::move(reader), true);
}
+1 -1
View File
@@ -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;
}
+5 -4
View File
@@ -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);
+5 -6
View File
@@ -8,6 +8,7 @@
#include <map>
#include <memory>
#include <optional>
#include <span>
#include <string>
#include <vector>
@@ -96,7 +97,6 @@ public:
}
virtual std::string GetGameID(const Partition& partition = PARTITION_NONE) const = 0;
virtual std::string GetGameTDBID(const Partition& partition = PARTITION_NONE) const = 0;
virtual std::string GetTriforceID() const { return ""; }
virtual std::string GetMakerID(const Partition& partition = PARTITION_NONE) const = 0;
virtual std::optional<u16> GetRevision(const Partition& partition = PARTITION_NONE) const = 0;
virtual std::string GetInternalName(const Partition& partition = PARTITION_NONE) const = 0;
@@ -144,16 +144,15 @@ public:
virtual std::array<u8, 20> GetSyncHash() const = 0;
protected:
template <u32 N>
std::string DecodeString(const char (&data)[N]) const
std::string DecodeString(std::span<const char> data) const
{
// strnlen to trim NULLs
std::string string(data, strnlen(data, sizeof(data)));
std::string string(data.data(), strnlen(data.data(), data.size()));
if (GetRegion() == Region::NTSC_J)
return SHIFTJISToUTF8(string);
else
return CP1252ToUTF8(string);
return CP1252ToUTF8(string);
}
void ReadAndAddToSyncHash(Common::SHA1::Context* context, u64 offset, u64 length,
+95 -3
View File
@@ -12,12 +12,46 @@
#include "DiscIO/DiscUtils.h"
#include "DiscIO/Enums.h"
#include "DiscIO/Filesystem.h"
#include "DiscIO/VolumeGC.h"
namespace DiscIO
{
std::string VolumeDisc::GetGameID(const Partition& partition) const
{
char id[6];
char id[6]{};
if (GetVolumeType() == Platform::Triforce)
{
// Triforce games have their Game ID stored in the boot.id file
const BootID* boot_id = static_cast<const VolumeGC*>(this)->GetTriforceBootID();
// Construct game ID from the BTID
id[0] = 'G';
memcpy(id + 1, boot_id->game_id.data() + 2, 2);
switch (GetCountry())
{
default:
case Country::Japan:
id[3] = 'J';
break;
case Country::Taiwan:
id[3] = 'W';
break;
case Country::USA:
id[3] = 'E';
break;
case Country::Europe:
id[3] = 'P';
break;
}
const std::string maker_id{GetMakerID()};
memcpy(id + 4, maker_id.c_str(), std::min<std::size_t>(maker_id.size(), 2));
return DecodeString(id);
}
if (!Read(0, sizeof(id), reinterpret_cast<u8*>(id), partition))
return std::string();
@@ -27,8 +61,35 @@ std::string VolumeDisc::GetGameID(const Partition& partition) const
Country VolumeDisc::GetCountry(const Partition& partition) const
{
// The 0 that we use as a default value is mapped to Country::Unknown and Region::Unknown
const u8 country_byte = ReadSwapped<u8>(3, partition).value_or(0);
u8 country_byte = 0;
if (GetVolumeType() == Platform::Triforce)
{
const BootID* boot_id = static_cast<const VolumeGC*>(this)->GetTriforceBootID();
switch (boot_id->region_flags)
{
default:
case 0x02: // JAPAN
country_byte = 'J';
break;
case 0x08: // ASIA
country_byte = 'W';
break;
case 0x0E: // USA
country_byte = 'E';
break;
case 0x0C: // EXPORT
country_byte = 'P';
break;
}
}
else
{
// The 0 that we use as a default value is mapped to Country::Unknown and Region::Unknown
country_byte = ReadSwapped<u8>(3, partition).value_or(0);
}
const Region region = GetRegion();
const std::optional<u16> revision = GetRevision();
@@ -51,6 +112,29 @@ std::string VolumeDisc::GetMakerID(const Partition& partition) const
{
char maker_id[2];
// Triforce games have their maker stored in the boot.id file as an actual string
if (GetVolumeType() == Platform::Triforce)
{
const BootID* boot_id = static_cast<const VolumeGC*>(this)->GetTriforceBootID();
// There only seem to be three different makers here,
// so we can just check the first char to difference between them.
//
// NAMCO CORPORATION, SEGA CORPORATION and Hitmaker co,ltd.
switch (boot_id->manufacturer[0])
{
case 'S': // SEGA CORPORATION
case 'H': // Hitmaker co,ltd
return DecodeString("6E");
case 'N': // NAMCO CORPORATION
return DecodeString("82");
default:
break;
}
// Fall back to normal maker from header
}
if (!Read(0x4, sizeof(maker_id), reinterpret_cast<u8*>(&maker_id), partition))
return std::string();
@@ -66,6 +150,14 @@ std::optional<u16> VolumeDisc::GetRevision(const Partition& partition) const
std::string VolumeDisc::GetInternalName(const Partition& partition) const
{
char name[0x60];
// Triforce games have their Title stored in the boot.id file
if (GetVolumeType() == Platform::Triforce)
{
const BootID* boot_id = static_cast<const VolumeGC*>(this)->GetTriforceBootID();
return DecodeString(boot_id->game_name);
}
if (!Read(0x20, sizeof(name), reinterpret_cast<u8*>(&name), partition))
return std::string();
+27
View File
@@ -3,6 +3,7 @@
#pragma once
#include <array>
#include <optional>
#include <string>
@@ -15,6 +16,30 @@ namespace DiscIO
class VolumeDisc : public Volume
{
public:
struct BootID
{
u32 magic; // 0x00 (Always BTID)
std::array<uint32_t, 3> unknown0;
std::array<uint32_t, 4> unknown1;
std::array<char, 4> mediaboard_type; // 0x20 (GCAM for Triforce)
uint32_t unknown2;
uint16_t year; // 0x28
uint8_t month; // 0x2A
uint8_t day; // 0x2B
uint8_t video_mode; // 0x2C unknown bitmask, resolutions + horizontal/vertical
uint8_t unknown3;
uint8_t type_3_compatible; // 0x2E (Type-3 compatible titles have this set to 1)
uint8_t unknown4;
std::array<char, 8> game_id; // 0x30
uint32_t region_flags; // 0x38
std::array<uint32_t, 9> unknown6;
std::array<char, 0x20> manufacturer; // 0x60
std::array<char, 0x20> game_name; // 0x80
std::array<char, 0x20> game_executable; // 0xA0
std::array<char, 0x20> test_executable; // 0xC0
std::array<std::array<char, 0x20>, 8> credit_types; // 0xE0
};
std::string GetGameID(const Partition& partition = PARTITION_NONE) const override;
Country GetCountry(const Partition& partition = PARTITION_NONE) const override;
std::string GetMakerID(const Partition& partition = PARTITION_NONE) const override;
@@ -29,4 +54,6 @@ protected:
void AddGamePartitionToSyncHash(Common::SHA1::Context* context) const;
};
static_assert(sizeof(VolumeDisc::BootID) == 480);
} // namespace DiscIO
+23 -12
View File
@@ -45,13 +45,11 @@ VolumeGC::VolumeGC(std::unique_ptr<BlobReader> reader)
std::unique_ptr<FileInfo> file_info = tmp_fs->FindFileInfo("boot.id");
if (!file_info)
return;
BootID triforce_header;
const u64 file_size = ReadFile(*this, PARTITION_NONE, file_info.get(),
reinterpret_cast<u8*>(&triforce_header), sizeof(BootID));
if (file_size >= 4 && triforce_header.magic == BTID_MAGIC)
reinterpret_cast<u8*>(&m_triforce_header), sizeof(BootID));
if (file_size >= 4 && m_triforce_header.magic == BTID_MAGIC)
{
m_is_triforce = true;
m_triforce_id = triforce_header.id;
}
}
}
@@ -90,16 +88,23 @@ std::string VolumeGC::GetGameTDBID(const Partition& partition) const
return GetGameID(partition);
}
std::string VolumeGC::GetTriforceID() const
{
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<u32>(0x458));
}
@@ -184,6 +189,12 @@ std::array<u8, 20> VolumeGC::GetSyncHash() const
VolumeGC::ConvertedGCBanner VolumeGC::LoadBannerFile() const
{
// There is at least one Triforce game that has an opening.bnr file but from a different game.
if (m_is_triforce)
{
return {};
}
GCBanner banner_file;
const u64 file_size = ReadFile(*this, PARTITION_NONE, "opening.bnr",
reinterpret_cast<u8*>(&banner_file), sizeof(GCBanner));
+4 -10
View File
@@ -34,7 +34,6 @@ public:
const Partition& partition = PARTITION_NONE) const override;
const FileSystem* GetFileSystem(const Partition& partition = PARTITION_NONE) const override;
std::string GetGameTDBID(const Partition& partition = PARTITION_NONE) const override;
std::string GetTriforceID() const override;
std::map<Language, std::string> GetShortNames() const override;
std::map<Language, std::string> GetLongNames() const override;
std::map<Language, std::string> GetShortMakers() const override;
@@ -50,9 +49,10 @@ public:
DataSizeType GetDataSizeType() const override;
u64 GetRawSize() const override;
const BlobReader& GetBlobReader() const override;
std::array<u8, 20> GetSyncHash() const override;
const BootID* GetTriforceBootID() const { return &m_triforce_header; }
private:
static constexpr u32 GC_BANNER_WIDTH = 96;
static constexpr u32 GC_BANNER_HEIGHT = 32;
@@ -77,13 +77,6 @@ private:
// (only one for BNR1 type)
};
struct BootID
{
u32 magic; // "BTID"
u32 padding[11];
std::array<char, 4> id;
};
struct ConvertedGCBanner
{
ConvertedGCBanner();
@@ -112,8 +105,9 @@ private:
std::unique_ptr<BlobReader> m_reader;
BootID m_triforce_header;
bool m_is_triforce;
std::array<char, 4> m_triforce_id;
};
} // namespace DiscIO
+10
View File
@@ -279,6 +279,7 @@
<ClInclude Include="Core\HW\DSPLLE\DSPDebugInterface.h" />
<ClInclude Include="Core\HW\DSPLLE\DSPLLE.h" />
<ClInclude Include="Core\HW\DSPLLE\DSPSymbols.h" />
<ClInclude Include="Core\HW\DVD\AMMediaboard.h" />
<ClInclude Include="Core\HW\DVD\DVDInterface.h" />
<ClInclude Include="Core\HW\DVD\DVDMath.h" />
<ClInclude Include="Core\HW\DVD\DVDThread.h" />
@@ -316,12 +317,16 @@
<ClInclude Include="Core\HW\HSP\HSP_DeviceARAMExpansion.h" />
<ClInclude Include="Core\HW\HSP\HSP_DeviceNull.h" />
<ClInclude Include="Core\HW\HW.h" />
<ClInclude Include="Core\HW\MagCard\C1231BR.h" />
<ClInclude Include="Core\HW\MagCard\C1231LR.h" />
<ClInclude Include="Core\HW\MagCard\MagneticCardReader.h" />
<ClInclude Include="Core\HW\Memmap.h" />
<ClInclude Include="Core\HW\MemoryInterface.h" />
<ClInclude Include="Core\HW\MMIO.h" />
<ClInclude Include="Core\HW\MMIOHandlers.h" />
<ClInclude Include="Core\HW\ProcessorInterface.h" />
<ClInclude Include="Core\HW\SI\SI_Device.h" />
<ClInclude Include="Core\HW\SI\SI_DeviceAMBaseboard.h" />
<ClInclude Include="Core\HW\SI\SI_DeviceDanceMat.h" />
<ClInclude Include="Core\HW\SI\SI_DeviceGBA.h" />
<ClInclude Include="Core\HW\SI\SI_DeviceGBAEmu.h" />
@@ -971,6 +976,7 @@
<ClCompile Include="Core\HW\DSPLLE\DSPHost.cpp" />
<ClCompile Include="Core\HW\DSPLLE\DSPLLE.cpp" />
<ClCompile Include="Core\HW\DSPLLE\DSPSymbols.cpp" />
<ClCompile Include="Core\HW\DVD\AMMediaboard.cpp" />
<ClCompile Include="Core\HW\DVD\DVDInterface.cpp" />
<ClCompile Include="Core\HW\DVD\DVDMath.cpp" />
<ClCompile Include="Core\HW\DVD\DVDThread.cpp" />
@@ -1012,11 +1018,15 @@
<ClCompile Include="Core\HW\HSP\HSP_DeviceARAMExpansion.cpp" />
<ClCompile Include="Core\HW\HSP\HSP_DeviceNull.cpp" />
<ClCompile Include="Core\HW\HW.cpp" />
<ClCompile Include="Core\HW\MagCard\C1231BR.cpp" />
<ClCompile Include="Core\HW\MagCard\C1231LR.cpp" />
<ClCompile Include="Core\HW\MagCard\MagneticCardReader.cpp" />
<ClCompile Include="Core\HW\Memmap.cpp" />
<ClCompile Include="Core\HW\MemoryInterface.cpp" />
<ClCompile Include="Core\HW\MMIO.cpp" />
<ClCompile Include="Core\HW\ProcessorInterface.cpp" />
<ClCompile Include="Core\HW\SI\SI_Device.cpp" />
<ClCompile Include="Core\HW\SI\SI_DeviceAMBaseboard.cpp" />
<ClCompile Include="Core\HW\SI\SI_DeviceDanceMat.cpp" />
<ClCompile Include="Core\HW\SI\SI_DeviceGBA.cpp" />
<ClCompile Include="Core\HW\SI\SI_DeviceGBAEmu.cpp" />
+2
View File
@@ -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
@@ -42,6 +42,7 @@ static constexpr std::array s_gc_types = {
#endif
SIDeviceName{SerialInterface::SIDEVICE_GC_GBA, _trans("GBA (TCP)")},
SIDeviceName{SerialInterface::SIDEVICE_GC_KEYBOARD, _trans("Keyboard Controller")},
SIDeviceName{SerialInterface::SIDEVICE_AM_BASEBOARD, _trans("Triforce Baseboard")},
};
static std::optional<int> ToGCMenuIndex(const SerialInterface::SIDevices sidevice)
@@ -156,6 +157,9 @@ void GamecubeControllersWidget::OnGCPadConfigure(size_t index)
case SerialInterface::SIDEVICE_GC_KEYBOARD:
type = MappingWindow::Type::MAPPING_GC_KEYBOARD;
break;
case SerialInterface::SIDEVICE_AM_BASEBOARD:
type = MappingWindow::Type::MAPPING_AM_BASEBOARD;
break;
default:
return;
}
@@ -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);
@@ -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);
@@ -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};
}
@@ -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;
}
@@ -45,6 +45,8 @@ public:
MAPPING_HOTKEYS,
// Freelook
MAPPING_FREELOOK,
// Triforce
MAPPING_AM_BASEBOARD,
};
explicit MappingWindow(QWidget* parent, Type type, int port_num);
@@ -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();
@@ -49,6 +49,7 @@ enum class SettingsWindowPaneIndex : int
Paths,
GameCube,
Wii,
Triforce,
Advanced,
};
+142 -64
View File
@@ -21,9 +21,13 @@
#include <sys/socket.h>
#endif
#include <bit>
#include "Common/FileUtil.h"
#include "Common/Network.h"
#include "Core/Config/MainSettings.h"
#include "Core/Core.h"
#include "Core/HW/DVD/AMMediaboard.h"
#include "Core/IOS/Network/SSL.h"
#include "Core/IOS/Network/Socket.h"
#include "Core/System.h"
@@ -96,7 +100,7 @@ QTableWidgetItem* GetSocketState(s32 host_fd)
return new QTableWidgetItem(QTableWidget::tr("Unbound"));
}
static QTableWidgetItem* GetSocketBlocking(const IOS::HLE::WiiSockMan& socket_manager, s32 wii_fd)
QTableWidgetItem* GetSocketBlocking(const IOS::HLE::WiiSockMan& socket_manager, s32 wii_fd)
{
if (socket_manager.GetHostSocket(wii_fd) < 0)
return new QTableWidgetItem();
@@ -104,7 +108,7 @@ static QTableWidgetItem* GetSocketBlocking(const IOS::HLE::WiiSockMan& socket_ma
return new QTableWidgetItem(is_blocking ? QTableWidget::tr("Yes") : QTableWidget::tr("No"));
}
static QString GetAddressAndPort(const sockaddr_in& addr)
QString GetAddressAndPort(const sockaddr_in& addr)
{
char buffer[16];
const char* addr_str = inet_ntop(AF_INET, &addr.sin_addr, buffer, sizeof(buffer));
@@ -139,6 +143,50 @@ QTableWidgetItem* GetSocketName(s32 host_fd)
return new QTableWidgetItem(QStringLiteral("%1->%2").arg(sock_name).arg(peer_name));
}
QTableWidgetItem* GetSocketRedirections(s32 host_fd,
const AMMediaboard::IPRedirections& ip_redirections)
{
if (host_fd < 0 || ip_redirections.empty())
return new QTableWidgetItem();
sockaddr_in sock_addr;
socklen_t sock_addr_len = sizeof(sockaddr_in);
if (getsockname(host_fd, reinterpret_cast<sockaddr*>(&sock_addr), &sock_addr_len) != 0)
return new QTableWidgetItem();
const Common::IPv4Port sock_ip_port{std::bit_cast<Common::IPAddress>(sock_addr.sin_addr),
sock_addr.sin_port};
sockaddr_in peer_addr;
socklen_t peer_addr_len = sizeof(sockaddr_in);
bool has_peer =
getpeername(host_fd, reinterpret_cast<sockaddr*>(&peer_addr), &peer_addr_len) == 0;
const Common::IPv4Port peer_ip_port =
has_peer ? Common::IPv4Port{std::bit_cast<Common::IPAddress>(peer_addr.sin_addr),
peer_addr.sin_port} :
Common::IPv4Port{};
QStringList sock_rules;
QStringList peer_rules;
for (const auto& rule : ip_redirections)
{
if (rule.replacement.IsMatch(sock_ip_port))
sock_rules << QString::fromStdString(rule.ToString());
if (!has_peer)
continue;
if (rule.replacement.IsMatch(peer_ip_port))
peer_rules << QString::fromStdString(rule.ToString());
}
if (sock_rules.isEmpty() && peer_rules.isEmpty())
return new QTableWidgetItem();
if (peer_rules.isEmpty())
return new QTableWidgetItem(sock_rules.join(QStringLiteral("\n")));
return new QTableWidgetItem(QStringLiteral("Sock rules:\n%1\n"
"\nPeer rules:%2")
.arg(sock_rules.join(QStringLiteral("\n")))
.arg(peer_rules.join(QStringLiteral("\n"))));
}
} // namespace
NetworkWidget::NetworkWidget(QWidget* parent) : QDockWidget(parent)
@@ -184,7 +232,7 @@ void NetworkWidget::closeEvent(QCloseEvent*)
Settings::Instance().SetNetworkVisible(false);
}
void NetworkWidget::showEvent(QShowEvent* event)
void NetworkWidget::showEvent(QShowEvent*)
{
Update();
}
@@ -208,45 +256,34 @@ void NetworkWidget::ConnectWidgets()
{
connect(m_dump_format_combo, &QComboBox::currentIndexChanged, this,
&NetworkWidget::OnDumpFormatComboChanged);
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
connect(m_dump_ssl_read_checkbox, &QCheckBox::checkStateChanged, [](Qt::CheckState state) {
Config::SetBaseOrCurrent(Config::MAIN_NETWORK_SSL_DUMP_READ, state == Qt::Checked);
});
connect(m_dump_ssl_write_checkbox, &QCheckBox::checkStateChanged, [](Qt::CheckState state) {
Config::SetBaseOrCurrent(Config::MAIN_NETWORK_SSL_DUMP_WRITE, state == Qt::Checked);
});
connect(m_dump_root_ca_checkbox, &QCheckBox::checkStateChanged, [](Qt::CheckState state) {
Config::SetBaseOrCurrent(Config::MAIN_NETWORK_SSL_DUMP_ROOT_CA, state == Qt::Checked);
});
connect(m_dump_peer_cert_checkbox, &QCheckBox::checkStateChanged, [](Qt::CheckState state) {
Config::SetBaseOrCurrent(Config::MAIN_NETWORK_SSL_DUMP_PEER_CERT, state == Qt::Checked);
});
connect(m_verify_certificates_checkbox, &QCheckBox::checkStateChanged, [](Qt::CheckState state) {
Config::SetBaseOrCurrent(Config::MAIN_NETWORK_SSL_VERIFY_CERTIFICATES, state == Qt::Checked);
});
connect(m_dump_bba_checkbox, &QCheckBox::checkStateChanged, [](Qt::CheckState state) {
Config::SetBaseOrCurrent(Config::MAIN_NETWORK_DUMP_BBA, state == Qt::Checked);
});
using CheckState = Qt::CheckState;
static constexpr auto checkStateChanged = &QCheckBox::checkStateChanged;
#else
connect(m_dump_ssl_read_checkbox, &QCheckBox::stateChanged, [](int state) {
using CheckState = int;
static constexpr auto checkStateChanged = &QCheckBox::stateChanged;
#endif
connect(m_dump_ssl_read_checkbox, checkStateChanged, [](CheckState state) {
Config::SetBaseOrCurrent(Config::MAIN_NETWORK_SSL_DUMP_READ, state == Qt::Checked);
});
connect(m_dump_ssl_write_checkbox, &QCheckBox::stateChanged, [](int state) {
connect(m_dump_ssl_write_checkbox, checkStateChanged, [](CheckState state) {
Config::SetBaseOrCurrent(Config::MAIN_NETWORK_SSL_DUMP_WRITE, state == Qt::Checked);
});
connect(m_dump_root_ca_checkbox, &QCheckBox::stateChanged, [](int state) {
connect(m_dump_root_ca_checkbox, checkStateChanged, [](CheckState state) {
Config::SetBaseOrCurrent(Config::MAIN_NETWORK_SSL_DUMP_ROOT_CA, state == Qt::Checked);
});
connect(m_dump_peer_cert_checkbox, &QCheckBox::stateChanged, [](int state) {
connect(m_dump_peer_cert_checkbox, checkStateChanged, [](CheckState state) {
Config::SetBaseOrCurrent(Config::MAIN_NETWORK_SSL_DUMP_PEER_CERT, state == Qt::Checked);
});
connect(m_verify_certificates_checkbox, &QCheckBox::stateChanged, [](int state) {
connect(m_verify_certificates_checkbox, checkStateChanged, [](CheckState state) {
Config::SetBaseOrCurrent(Config::MAIN_NETWORK_SSL_VERIFY_CERTIFICATES, state == Qt::Checked);
});
connect(m_dump_bba_checkbox, &QCheckBox::stateChanged, [](int state) {
connect(m_dump_bba_checkbox, checkStateChanged, [](CheckState state) {
Config::SetBaseOrCurrent(Config::MAIN_NETWORK_DUMP_BBA, state == Qt::Checked);
});
#endif
connect(m_open_dump_folder, &QPushButton::clicked, [] {
const std::string location = File::GetUserPath(D_DUMPSSL_IDX);
const QUrl url = QUrl::fromLocalFile(QString::fromStdString(location));
@@ -254,24 +291,12 @@ void NetworkWidget::ConnectWidgets()
});
}
void NetworkWidget::Update()
void NetworkWidget::UpdateWiiSocketTable(Core::System& system)
{
if (!isVisible())
return;
auto& system = Core::System::GetInstance();
if (Core::GetState(system) != Core::State::Paused)
{
m_socket_table->setDisabled(true);
m_ssl_table->setDisabled(true);
return;
}
m_socket_table->setDisabled(false);
m_ssl_table->setDisabled(false);
// needed because there's a race condition on the IOS instance otherwise
const Core::CPUThreadGuard guard(system);
// Show Wii socket blocking state
m_socket_table->showColumn(4);
// Hide Triforce IP redirections
m_socket_table->hideColumn(6);
auto* ios = system.GetIOS();
if (!ios)
@@ -281,7 +306,6 @@ void NetworkWidget::Update()
if (!socket_manager)
return;
m_socket_table->setRowCount(0);
for (s32 wii_fd = 0; wii_fd < IOS::HLE::WII_SOCKET_FD_MAX; wii_fd++)
{
m_socket_table->insertRow(wii_fd);
@@ -293,9 +317,7 @@ void NetworkWidget::Update()
m_socket_table->setItem(wii_fd, 4, GetSocketBlocking(*socket_manager, wii_fd));
m_socket_table->setItem(wii_fd, 5, GetSocketName(host_fd));
}
m_socket_table->resizeColumnsToContents();
m_ssl_table->setRowCount(0);
for (s32 ssl_id = 0; ssl_id < IOS::HLE::NET_SSL_MAXINSTANCES; ssl_id++)
{
m_ssl_table->insertRow(ssl_id);
@@ -312,6 +334,58 @@ void NetworkWidget::Update()
m_ssl_table->setItem(ssl_id, 3, GetSocketState(host_fd));
m_ssl_table->setItem(ssl_id, 4, GetSocketName(host_fd));
}
}
void NetworkWidget::UpdateTriforceSocketTable()
{
// No easy way to get socket blocking state on Windows
m_socket_table->hideColumn(4);
// Show active IP redirections
m_socket_table->showColumn(6);
const auto ip_redirections = AMMediaboard::GetIPRedirections();
for (s32 triforce_fd = 0; triforce_fd != AMMediaboard::SOCKET_FD_MAX; ++triforce_fd)
{
m_socket_table->insertRow(triforce_fd);
const s32 host_fd = AMMediaboard::DebuggerGetSocket(triforce_fd);
m_socket_table->setItem(triforce_fd, 0, new QTableWidgetItem(QString::number(triforce_fd)));
m_socket_table->setItem(triforce_fd, 1, GetSocketDomain(host_fd));
m_socket_table->setItem(triforce_fd, 2, GetSocketType(host_fd));
m_socket_table->setItem(triforce_fd, 3, GetSocketState(host_fd));
m_socket_table->setItem(triforce_fd, 4, new QTableWidgetItem(QTableWidget::tr("Unknown")));
m_socket_table->setItem(triforce_fd, 5, GetSocketName(host_fd));
m_socket_table->setItem(triforce_fd, 6, GetSocketRedirections(host_fd, ip_redirections));
}
}
void NetworkWidget::Update()
{
if (!isVisible())
return;
auto& system = Core::System::GetInstance();
if (Core::GetState(system) != Core::State::Paused)
{
m_socket_table->setDisabled(true);
m_ssl_table->setDisabled(true);
return;
}
// needed because there's a race condition on the IOS instance otherwise
const Core::CPUThreadGuard guard(system);
m_socket_table->setDisabled(false);
m_socket_table->setRowCount(0);
m_ssl_table->setRowCount(0);
if (system.IsTriforce())
{
UpdateTriforceSocketTable();
}
else if (system.IsWii())
{
m_ssl_table->setDisabled(false);
UpdateWiiSocketTable(system);
}
m_socket_table->resizeColumnsToContents();
m_socket_table->resizeRowsToContents();
m_ssl_table->resizeColumnsToContents();
const bool is_pcap = Config::Get(Config::MAIN_NETWORK_DUMP_AS_PCAP);
@@ -328,27 +402,31 @@ void NetworkWidget::Update()
const int combo_index = int([is_pcap, is_ssl_read, is_ssl_write]() -> FormatComboId {
if (is_pcap)
return FormatComboId::PCAP;
else if (is_ssl_read && is_ssl_write)
if (is_ssl_read && is_ssl_write)
return FormatComboId::BinarySSL;
else if (is_ssl_read)
if (is_ssl_read)
return FormatComboId::BinarySSLRead;
else if (is_ssl_write)
if (is_ssl_write)
return FormatComboId::BinarySSLWrite;
else
return FormatComboId::None;
return FormatComboId::None;
}());
m_dump_format_combo->setCurrentIndex(combo_index);
}
QGroupBox* NetworkWidget::CreateSocketTableGroup()
{
QGroupBox* socket_table_group = new QGroupBox(tr("Socket table"));
QGridLayout* socket_table_layout = new QGridLayout;
auto* const socket_table_group = new QGroupBox(tr("Socket table"));
auto* const socket_table_layout = new QGridLayout;
socket_table_group->setLayout(socket_table_layout);
m_socket_table = new QTableWidget();
// i18n: FD stands for file descriptor (and in this case refers to sockets, not regular files)
QStringList header{tr("FD"), tr("Domain"), tr("Type"), tr("State"), tr("Blocking"), tr("Name")};
QStringList header{tr("FD"), tr("Domain"), tr("Type"), tr("State"),
tr("Blocking"), tr("Name"), tr("Redirections")};
m_socket_table->setColumnCount(static_cast<int>(header.size()));
m_socket_table->setHorizontalHeaderLabels(header);
@@ -365,8 +443,8 @@ QGroupBox* NetworkWidget::CreateSocketTableGroup()
QGroupBox* NetworkWidget::CreateSSLContextGroup()
{
QGroupBox* ssl_context_group = new QGroupBox(tr("SSL context"));
QGridLayout* ssl_context_layout = new QGridLayout;
auto* const ssl_context_group = new QGroupBox(tr("SSL context"));
auto* const ssl_context_layout = new QGridLayout;
ssl_context_group->setLayout(ssl_context_layout);
m_ssl_table = new QTableWidget();
@@ -387,8 +465,8 @@ QGroupBox* NetworkWidget::CreateSSLContextGroup()
QGroupBox* NetworkWidget::CreateDumpOptionsGroup()
{
auto* dump_options_group = new QGroupBox(tr("Dump options"));
auto* dump_options_layout = new QVBoxLayout;
auto* const dump_options_group = new QGroupBox(tr("Dump options"));
auto* const dump_options_layout = new QVBoxLayout;
dump_options_group->setLayout(dump_options_layout);
m_dump_format_combo = CreateDumpFormatCombo();
@@ -402,9 +480,9 @@ QGroupBox* NetworkWidget::CreateDumpOptionsGroup()
m_open_dump_folder = new QPushButton(tr("Open dump folder"));
m_open_dump_folder->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
auto* combo_label = new QLabel(tr("Network dump format:"));
auto* const combo_label = new QLabel(tr("Network dump format:"));
combo_label->setBuddy(m_dump_format_combo);
auto* combo_layout = new QHBoxLayout;
auto* const combo_layout = new QHBoxLayout;
combo_layout->addWidget(combo_label);
const int combo_label_space =
combo_label->fontMetrics().boundingRect(QStringLiteral("__")).width();
@@ -426,8 +504,8 @@ QGroupBox* NetworkWidget::CreateDumpOptionsGroup()
QGroupBox* NetworkWidget::CreateSecurityOptionsGroup()
{
auto* security_options_group = new QGroupBox(tr("Security options"));
auto* security_options_layout = new QVBoxLayout;
auto* const security_options_group = new QGroupBox(tr("Security options"));
auto* const security_options_layout = new QVBoxLayout;
security_options_group->setLayout(security_options_layout);
m_verify_certificates_checkbox = new QCheckBox(tr("Verify certificates"));
@@ -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();
+3 -1
View File
@@ -223,6 +223,7 @@
<ClCompile Include="Settings\GeneralPane.cpp" />
<ClCompile Include="Settings\InterfacePane.cpp" />
<ClCompile Include="Settings\OnScreenDisplayPane.cpp" />
<ClCompile Include="Settings\TriforcePane.cpp" />
<ClCompile Include="Settings\PathPane.cpp" />
<ClCompile Include="Settings\USBDevicePicker.cpp" />
<ClCompile Include="Settings\WiiPane.cpp" />
@@ -439,6 +440,7 @@
<QtMoc Include="Settings\GeneralPane.h" />
<QtMoc Include="Settings\InterfacePane.h" />
<QtMoc Include="Settings\PathPane.h" />
<QtMoc Include="Settings\TriforcePane.h" />
<QtMoc Include="Settings\USBDevicePicker.h" />
<QtMoc Include="Settings\WiiPane.h" />
<QtMoc Include="SkylanderPortal\SkylanderPortalWindow.h" />
@@ -523,4 +525,4 @@
<Message Text="Copy: @(BinaryFiles) -&gt; $(BinaryOutputDir)" Importance="High" />
<Copy SourceFiles="@(BinaryFiles)" DestinationFolder="$(BinaryOutputDir)" />
</Target>
</Project>
</Project>
+32
View File
@@ -1151,6 +1151,32 @@ void MainWindow::StartGame(std::unique_ptr<BootParameters>&& parameters)
if (!NKitWarningDialog::ShowUnlessDisabled())
return;
}
const auto volume_type =
std::get<BootParameters::Disc>(parameters->parameters).volume->GetVolumeType();
if (volume_type != DiscIO::Platform::Triforce)
{
const bool triforce_hardware_sp1 =
Config::Get(Config::MAIN_SERIAL_PORT_1) == ExpansionInterface::EXIDeviceType::Baseboard;
const bool triforce_hardware_port_1 = Config::Get(Config::GetInfoForSIDevice(0)) ==
SerialInterface::SIDevices::SIDEVICE_AM_BASEBOARD;
// Some Triforce tools don't include a boot.id file, but they can still be launched.
if (triforce_hardware_sp1)
{
ModalMessageBox::warning(this, tr("Warning"),
tr("Non-Triforce games cannot be booted with Triforce hardware "
"attached.\nPlease remove the Triforce Baseboard from SP1."),
QMessageBox::Ok);
}
if (triforce_hardware_port_1)
{
ModalMessageBox::warning(this, tr("Warning"),
tr("Non-Triforce games cannot be booted with Triforce hardware "
"attached.\nPlease remove the Triforce Baseboard from Port 1."),
QMessageBox::Ok);
}
}
}
// If we're running, only start a new game once we've stopped the last.
@@ -1289,6 +1315,12 @@ void MainWindow::ShowControllersWindow()
m_settings_window->SelectPane(SettingsWindowPaneIndex::Controllers);
}
void MainWindow::ShowTriforceWindow()
{
ShowSettingsWindow();
m_settings_window->SelectPane(SettingsWindowPaneIndex::Triforce);
}
void MainWindow::ShowFreeLookWindow()
{
if (!m_freelook_window)
+2
View File
@@ -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);
+4
View File
@@ -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);
+1
View File
@@ -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;
+40 -21
View File
@@ -31,6 +31,7 @@
#include "Core/Core.h"
#include "Core/HW/EXI/EXI.h"
#include "Core/HW/GCMemcard/GCMemcard.h"
#include "DolphinQt/Config/SettingsWindow.h"
#ifdef HAS_LIBMGBA
#include "Core/NetPlayServer.h"
#endif
@@ -50,15 +51,18 @@
#include "DolphinQt/QtUtils/SignalBlocking.h"
#include "DolphinQt/Settings.h"
#include "DolphinQt/Settings/BroadbandAdapterSettingsDialog.h"
#include "DolphinQt/Settings/TriforcePane.h"
constexpr std::initializer_list<ExpansionInterface::Slot> GUI_SLOTS = {
ExpansionInterface::Slot::A, ExpansionInterface::Slot::B, ExpansionInterface::Slot::SP1};
GameCubePane::GameCubePane()
GameCubePane::GameCubePane(MainWindow* main_window)
{
CreateWidgets();
LoadSettings();
ConnectWidgets();
connect(this, &GameCubePane::ShowTriforceWindow, main_window, &MainWindow::ShowTriforceWindow);
}
void GameCubePane::CreateWidgets()
@@ -151,6 +155,7 @@ void GameCubePane::CreateWidgets()
EXIDeviceType::EthernetIPC,
#endif
EXIDeviceType::ModemTapServer,
EXIDeviceType::Baseboard,
})
{
m_slot_combos[ExpansionInterface::Slot::SP1]->addItem(tr(fmt::format("{:n}", device).c_str()),
@@ -350,11 +355,12 @@ void GameCubePane::UpdateButton(ExpansionInterface::Slot slot)
break;
}
case ExpansionInterface::Slot::SP1:
has_config = (device == ExpansionInterface::EXIDeviceType::Ethernet ||
device == ExpansionInterface::EXIDeviceType::EthernetXLink ||
device == ExpansionInterface::EXIDeviceType::EthernetTapServer ||
device == ExpansionInterface::EXIDeviceType::EthernetBuiltIn ||
device == ExpansionInterface::EXIDeviceType::ModemTapServer);
has_config = device == ExpansionInterface::EXIDeviceType::Ethernet ||
device == ExpansionInterface::EXIDeviceType::EthernetXLink ||
device == ExpansionInterface::EXIDeviceType::EthernetTapServer ||
device == ExpansionInterface::EXIDeviceType::EthernetBuiltIn ||
device == ExpansionInterface::EXIDeviceType::ModemTapServer ||
device == ExpansionInterface::EXIDeviceType::Baseboard;
break;
case ExpansionInterface::Slot::SP2:
has_config = false;
@@ -418,6 +424,11 @@ void GameCubePane::OnConfigPressed(ExpansionInterface::Slot slot)
dialog.exec();
return;
}
case ExpansionInterface::EXIDeviceType::Baseboard:
{
ShowTriforceWindow();
return;
}
default:
PanicAlertFmt("Unknown settings pressed for {}", device);
return;
@@ -452,19 +463,23 @@ bool GameCubePane::SetMemcard(ExpansionInterface::Slot slot, const QString& file
const std::string jp_path = Config::GetMemcardPath(raw_path, slot, DiscIO::Region::NTSC_J);
const std::string us_path = Config::GetMemcardPath(raw_path, slot, DiscIO::Region::NTSC_U);
const std::string eu_path = Config::GetMemcardPath(raw_path, slot, DiscIO::Region::PAL);
const bool raw_path_valid = raw_path == jp_path || raw_path == us_path || raw_path == eu_path;
const std::string dv_path = Config::GetMemcardPath(raw_path, slot, DiscIO::Region::DEV);
const bool raw_path_valid =
raw_path == jp_path || raw_path == us_path || raw_path == eu_path || raw_path == dv_path;
if (!raw_path_valid)
{
// TODO: We could try to autodetect the card region here and offer automatic renaming.
ModalMessageBox::critical(this, tr("Error"),
tr("The filename %1 does not conform to Dolphin's region code format "
"for memory cards. Please rename this file to either %2, %3, or "
"%4, matching the region of the save files that are on it.")
.arg(QString::fromStdString(PathToFileName(raw_path)))
.arg(QString::fromStdString(PathToFileName(us_path)))
.arg(QString::fromStdString(PathToFileName(eu_path)))
.arg(QString::fromStdString(PathToFileName(jp_path))));
ModalMessageBox::critical(
this, tr("Error"),
tr("The filename %1 does not conform to Dolphin's region code format "
"for memory cards. Please rename this file to either %2, %3, %4, or "
"%5, matching the region of the save files that are on it.")
.arg(QString::fromStdString(PathToFileName(raw_path)))
.arg(QString::fromStdString(PathToFileName(us_path)))
.arg(QString::fromStdString(PathToFileName(eu_path)))
.arg(QString::fromStdString(PathToFileName(jp_path)))
.arg(QString::fromStdString(PathToFileName(dv_path))));
return false;
}
@@ -556,8 +571,9 @@ bool GameCubePane::SetGCIFolder(ExpansionInterface::Slot slot, const QString& pa
const std::string default_jp_path = Config::GetGCIFolderPath("", slot, DiscIO::Region::NTSC_J);
const std::string default_us_path = Config::GetGCIFolderPath("", slot, DiscIO::Region::NTSC_U);
const std::string default_eu_path = Config::GetGCIFolderPath("", slot, DiscIO::Region::PAL);
const bool is_default_path =
raw_path == default_jp_path || raw_path == default_us_path || raw_path == default_eu_path;
const std::string default_dv_path = Config::GetGCIFolderPath("", slot, DiscIO::Region::DEV);
const bool is_default_path = raw_path == default_jp_path || raw_path == default_us_path ||
raw_path == default_eu_path || raw_path == default_dv_path;
bool path_changed;
if (is_default_path)
@@ -574,7 +590,9 @@ bool GameCubePane::SetGCIFolder(ExpansionInterface::Slot slot, const QString& pa
const std::string jp_path = Config::GetGCIFolderPath(raw_path, slot, DiscIO::Region::NTSC_J);
const std::string us_path = Config::GetGCIFolderPath(raw_path, slot, DiscIO::Region::NTSC_U);
const std::string eu_path = Config::GetGCIFolderPath(raw_path, slot, DiscIO::Region::PAL);
const bool raw_path_valid = raw_path == jp_path || raw_path == us_path || raw_path == eu_path;
const std::string dv_path = Config::GetGCIFolderPath(raw_path, slot, DiscIO::Region::DEV);
const bool raw_path_valid =
raw_path == jp_path || raw_path == us_path || raw_path == eu_path || raw_path == dv_path;
if (!raw_path_valid)
{
@@ -582,12 +600,13 @@ bool GameCubePane::SetGCIFolder(ExpansionInterface::Slot slot, const QString& pa
ModalMessageBox::critical(
this, tr("Error"),
tr("The folder %1 does not conform to Dolphin's region code format "
"for GCI folders. Please rename this folder to either %2, %3, or "
"%4, matching the region of the save files that are in it.")
"for GCI folders. Please rename this folder to either %2, %3, %4, or "
"%5, matching the region of the save files that are in it.")
.arg(QString::fromStdString(PathToFileName(raw_path)))
.arg(QString::fromStdString(PathToFileName(us_path)))
.arg(QString::fromStdString(PathToFileName(eu_path)))
.arg(QString::fromStdString(PathToFileName(jp_path))));
.arg(QString::fromStdString(PathToFileName(jp_path)))
.arg(QString::fromStdString(PathToFileName(dv_path))));
return false;
}
@@ -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();
@@ -0,0 +1,221 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "DolphinQt/Settings/TriforcePane.h"
#include <QDialogButtonBox>
#include <QGroupBox>
#include <QHeaderView>
#include <QPushButton>
#include <QTableWidget>
#include <QVBoxLayout>
#include <fmt/format.h>
#include <fmt/ranges.h>
#include "Common/Logging/Log.h"
#include "Core/Config/MainSettings.h"
#include "Core/HW/DVD/AMMediaboard.h"
#include "DolphinQt/Config/Mapping/MappingWindow.h"
#include "DolphinQt/QtUtils/NonDefaultQPushButton.h"
#include "DolphinQt/QtUtils/QtUtils.h"
namespace
{
constexpr int COLUMN_EMULATED = 0;
constexpr int COLUMN_REAL = 1;
constexpr int COLUMN_DESCRIPTION = 2;
class IPRedirectionsDialog final : public QDialog
{
public:
explicit IPRedirectionsDialog(QWidget* parent);
private:
void LoadConfig();
void SaveConfig() const;
void LoadDefault();
void OnClear();
QTableWidget* m_ip_redirections_table;
};
} // namespace
TriforcePane::TriforcePane()
{
auto* const main_layout = new QVBoxLayout{this};
auto* const controllers_group = new QGroupBox{tr("Controllers")};
main_layout->addWidget(controllers_group);
auto* const controllers_layout = new QVBoxLayout{controllers_group};
auto* const configure_controllers_button = new NonDefaultQPushButton{tr("Configure")};
controllers_layout->addWidget(configure_controllers_button);
connect(configure_controllers_button, &QPushButton::clicked, this, [this] {
auto* const window = new MappingWindow(this, MappingWindow::Type::MAPPING_AM_BASEBOARD, 0);
window->setAttribute(Qt::WA_DeleteOnClose, true);
window->setWindowModality(Qt::WindowModality::WindowModal);
window->show();
});
auto* const ip_redirection_group = new QGroupBox{tr("IP Address Redirections")};
main_layout->addWidget(ip_redirection_group);
auto* const ip_redirection_layout = new QVBoxLayout{ip_redirection_group};
auto* const configure_ip_redirections_button = new NonDefaultQPushButton{tr("Configure")};
ip_redirection_layout->addWidget(configure_ip_redirections_button);
connect(configure_ip_redirections_button, &QPushButton::clicked, this, [this] {
auto* const ip_redirections = new IPRedirectionsDialog{this};
ip_redirections->setAttribute(Qt::WA_DeleteOnClose);
ip_redirections->open();
});
main_layout->addStretch(1);
}
IPRedirectionsDialog::IPRedirectionsDialog(QWidget* parent) : QDialog{parent}
{
setWindowTitle(tr("IP Address Redirections"));
auto* const main_layout = new QVBoxLayout{this};
auto* const load_default_button = new QPushButton(tr("Default"));
connect(load_default_button, &QPushButton::clicked, this, &IPRedirectionsDialog::LoadDefault);
auto* const clear_button = new QPushButton(tr("Clear"));
connect(clear_button, &QPushButton::clicked, this, &IPRedirectionsDialog::OnClear);
m_ip_redirections_table = new QTableWidget;
main_layout->addWidget(m_ip_redirections_table);
m_ip_redirections_table->setColumnCount(3);
m_ip_redirections_table->setHorizontalHeaderLabels(
{tr("Emulated"), tr("Real"), tr("Description")});
m_ip_redirections_table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
m_ip_redirections_table->setSizeAdjustPolicy(QTableWidget::AdjustToContents);
m_ip_redirections_table->setEditTriggers(QAbstractItemView::DoubleClicked |
QAbstractItemView::EditKeyPressed);
auto* const button_box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
main_layout->addWidget(button_box);
button_box->addButton(load_default_button, QDialogButtonBox::ActionRole);
button_box->addButton(clear_button, QDialogButtonBox::ActionRole);
connect(button_box, &QDialogButtonBox::accepted, this, &IPRedirectionsDialog::accept);
connect(button_box, &QDialogButtonBox::rejected, this, &IPRedirectionsDialog::reject);
connect(this, &IPRedirectionsDialog::accepted, this, &IPRedirectionsDialog::SaveConfig);
connect(m_ip_redirections_table, &QTableWidget::itemChanged, this,
[this](QTableWidgetItem* item) {
const int row_count = m_ip_redirections_table->rowCount();
const bool is_last_row = item->row() == row_count - 1;
if (is_last_row)
{
if (!item->text().isEmpty())
m_ip_redirections_table->insertRow(row_count);
}
else if (item->column() != COLUMN_DESCRIPTION && item->text().isEmpty())
{
// Just remove inner rows that have text erased.
// This UX could be less weird, but it's good enough for now.
m_ip_redirections_table->removeRow(item->row());
}
});
LoadConfig();
QtUtils::AdjustSizeWithinScreen(this);
}
void IPRedirectionsDialog::LoadConfig()
{
m_ip_redirections_table->setRowCount(0);
// Add the final empty row.
m_ip_redirections_table->insertRow(0);
int row = 0;
const auto ip_redirections_str = Config::Get(Config::MAIN_TRIFORCE_IP_REDIRECTIONS);
for (auto&& ip_pair : ip_redirections_str | std::views::split(','))
{
const auto ip_pair_str = std::string_view{ip_pair};
const auto parsed = AMMediaboard::ParseIPRedirection(ip_pair_str);
if (!parsed.has_value())
{
ERROR_LOG_FMT(COMMON, "Bad IP redirection string: {}", ip_pair_str);
continue;
}
m_ip_redirections_table->insertRow(row);
m_ip_redirections_table->setItem(row, COLUMN_EMULATED,
new QTableWidgetItem(QString::fromUtf8(parsed->original)));
m_ip_redirections_table->setItem(row, COLUMN_REAL,
new QTableWidgetItem(QString::fromUtf8(parsed->replacement)));
m_ip_redirections_table->setItem(row, COLUMN_DESCRIPTION,
new QTableWidgetItem(QString::fromUtf8(parsed->description)));
++row;
}
}
void IPRedirectionsDialog::SaveConfig() const
{
// Ignore our empty row.
const int row_count = m_ip_redirections_table->rowCount() - 1;
std::vector<std::string> replacements(row_count);
for (int row = 0; row < row_count; ++row)
{
auto* const original_item = m_ip_redirections_table->item(row, COLUMN_EMULATED);
auto* const replacement_item = m_ip_redirections_table->item(row, COLUMN_REAL);
// Skip incomplete rows.
if (original_item == nullptr || replacement_item == nullptr)
continue;
replacements[row] = fmt::format("{}={}", StripWhitespace(original_item->text().toStdString()),
replacement_item->text().toStdString());
auto* const description_item = m_ip_redirections_table->item(row, COLUMN_DESCRIPTION);
if (description_item == nullptr)
continue;
const auto description = description_item->text().toStdString();
if (!description.empty())
(replacements[row] += ' ') += description;
}
Config::SetBaseOrCurrent(Config::MAIN_TRIFORCE_IP_REDIRECTIONS,
fmt::format("{}", fmt::join(replacements, ",")));
}
void IPRedirectionsDialog::LoadDefault()
{
// This alters the config before "OK" is pressed. Bad UX..
Config::SetBaseOrCurrent(Config::MAIN_TRIFORCE_IP_REDIRECTIONS, Config::DefaultState{});
LoadConfig();
}
void IPRedirectionsDialog::OnClear()
{
m_ip_redirections_table->setRowCount(0);
// Add the final empty row.
m_ip_redirections_table->insertRow(0);
}
@@ -0,0 +1,14 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QWidget>
class TriforcePane final : public QWidget
{
Q_OBJECT
public:
TriforcePane();
};
+9
View File
@@ -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;
+1 -4
View File
@@ -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;
}
-2
View File
@@ -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;
+1 -1
View File
@@ -23,7 +23,7 @@
namespace UICommon
{
static constexpr u32 CACHE_REVISION = 26; // Last changed in PR 10084
static constexpr u32 CACHE_REVISION = 27; // Last changed in PR 13844
std::vector<std::string> FindAllGamePaths(std::span<const std::string_view> directories_to_scan,
bool recursive_scan)
+1
View File
@@ -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));
+150
View File
@@ -6,6 +6,7 @@
#include <string>
#include <vector>
#include "Common/Network.h"
#include "Common/StringUtil.h"
#include "Common/Swap.h"
@@ -259,6 +260,155 @@ TEST(StringUtil, CaseInsensitiveContains_OverlappingMatches)
EXPECT_TRUE(Common::CaseInsensitiveContains("ababababa", "bABa"));
}
TEST(StringUtil, SplitStringIntoArray)
{
constexpr auto subject0 = "";
EXPECT_FALSE(SplitStringIntoArray<2>(subject0, '.').has_value());
constexpr auto subject1 = "hello";
EXPECT_FALSE(SplitStringIntoArray<2>(subject1, '.').has_value());
constexpr auto subject2 = "hello.world";
const auto split2 = SplitStringIntoArray<2>(subject2, '.');
EXPECT_TRUE(split2.has_value() && split2->at(1) == "world");
EXPECT_FALSE(SplitStringIntoArray<3>(subject2, '.').has_value());
constexpr auto subject3 = "hello.universe.";
EXPECT_FALSE(SplitStringIntoArray<2>(subject3, '.').has_value());
const auto split3 = SplitStringIntoArray<3>(subject3, '.');
EXPECT_TRUE(split3.has_value() && split3->at(2) == "");
EXPECT_FALSE(SplitStringIntoArray<4>(subject3, '.').has_value());
constexpr auto subject4 = "=";
EXPECT_TRUE(SplitStringIntoArray<2>(subject4, '=').has_value());
}
TEST(StringUtil, StringToIPv4PortRange)
{
constexpr auto parse = Common::StringToIPv4PortRange;
constexpr Common::IPAddress ip_network = {192, 168, 0, 0};
constexpr Common::IPAddress test_ip = {192, 168, 0, 13};
constexpr Common::IPAddress last_ip = {192, 168, 0, 255};
EXPECT_FALSE(parse("").has_value());
EXPECT_FALSE(parse("1.2.3").has_value());
EXPECT_FALSE(parse("1.2.3.4-").has_value());
EXPECT_FALSE(parse("1.2.3.4.5").has_value());
EXPECT_FALSE(parse("1.2.3.256").has_value());
EXPECT_FALSE(parse("1.2.3.4/33").has_value());
EXPECT_FALSE(parse("1.2.3.4:65536").has_value());
EXPECT_FALSE(parse("1.2.3.4-1.2.3.4.5").has_value());
EXPECT_FALSE(parse("1.1.1.1:81-80").has_value());
EXPECT_FALSE(parse("1.1.1.2-1.1.1.1").has_value());
EXPECT_TRUE(parse("1.2.3.4").has_value());
EXPECT_TRUE(parse("1.2.3.4:80").has_value());
EXPECT_TRUE(parse("1.2.3.4:80-81").has_value());
EXPECT_TRUE(parse("1.2.3.4/16").has_value());
EXPECT_TRUE(parse("1.2.3.4/16:80").has_value());
EXPECT_TRUE(parse("1.2.3.4/16:80-81").has_value());
EXPECT_TRUE(parse("1.2.3.4-10").has_value());
EXPECT_TRUE(parse("1.2.3.4-10:80").has_value());
EXPECT_TRUE(parse("1.2.3.4-10:80-81").has_value());
{
const auto simple_result = parse("192.168.0.13").value();
EXPECT_EQ(simple_result.first.ip_address, test_ip);
EXPECT_EQ(simple_result.last.ip_address, test_ip);
EXPECT_EQ(simple_result.first.GetPortValue(), 0);
EXPECT_EQ(simple_result.last.GetPortValue(), u16(-1));
}
{
const auto port_result = parse("192.168.0.13:77").value();
EXPECT_EQ(port_result.first.ip_address, test_ip);
EXPECT_EQ(port_result.last.ip_address, test_ip);
EXPECT_EQ(port_result.first.GetPortValue(), 77);
EXPECT_EQ(port_result.last.GetPortValue(), 77);
}
{
const auto ip_range_result = parse("192.168.0.13-192.168.0.255:123").value();
EXPECT_EQ(ip_range_result.first.ip_address, test_ip);
EXPECT_EQ(ip_range_result.last.ip_address, last_ip);
EXPECT_EQ(ip_range_result.first.GetPortValue(), 123);
EXPECT_EQ(ip_range_result.last.GetPortValue(), 123);
}
{
const auto port_range_result = parse("192.168.29.151:5000-5010").value();
EXPECT_EQ(port_range_result.first.GetPortValue(), 5000);
EXPECT_EQ(port_range_result.last.GetPortValue(), 5010);
EXPECT_TRUE(port_range_result.IsMatch(parse("192.168.29.151:5008")->first));
EXPECT_FALSE(port_range_result.IsMatch(parse("127.0.0.1:5000")->first));
}
{
const auto ip_range_result = parse("192.168.0.13-255:123-999").value();
EXPECT_EQ(ip_range_result.first.ip_address, test_ip);
EXPECT_EQ(ip_range_result.last.ip_address, last_ip);
EXPECT_EQ(ip_range_result.first.GetPortValue(), 123);
EXPECT_EQ(ip_range_result.last.GetPortValue(), 999);
}
{
const auto ip_network_result = parse("192.168.0.13/24:123-999").value();
EXPECT_EQ(ip_network_result.first.ip_address, ip_network);
EXPECT_EQ(ip_network_result.last.ip_address, last_ip);
EXPECT_EQ(ip_network_result.first.GetPortValue(), 123);
EXPECT_EQ(ip_network_result.last.GetPortValue(), 999);
}
EXPECT_TRUE(parse("192.168.0.13/24")->IsMatch(parse("192.168.0.99")->first));
EXPECT_FALSE(parse("192.168.0.13/24")->IsMatch(parse("192.168.1.99")->first));
EXPECT_FALSE(parse("192.168.0.13/32")->IsMatch(parse("192.168.0.99")->first));
EXPECT_TRUE(parse("192.168.0.13/0")->IsMatch(parse("10.0.0.1")->first));
EXPECT_TRUE(parse("192.168.0.13")->IsMatch(parse("192.168.0.13:81")->first));
EXPECT_FALSE(parse("192.168.0.13:80")->IsMatch(parse("192.168.0.13:81")->first));
EXPECT_TRUE(parse("192.168.0.13:80")->IsMatch(parse("192.168.0.13:80")->first));
EXPECT_FALSE(parse("192.168.0.13:80-81")->IsMatch(parse("192.168.0.14:81")->first));
EXPECT_TRUE(parse("192.168.0.13-14:80-81")->IsMatch(parse("192.168.0.14:81")->first));
}
TEST(StringUtil, IPv4PortRangeToString)
{
Common::IPv4PortRange subject{
.first = {{10, 3, 0, 127}, 0},
.last = {{10, 3, 0, 127}, 0},
};
EXPECT_EQ(subject.ToString(), "10.3.0.127");
subject.last.port = Common::swap16(80); // First port is still zero.
EXPECT_EQ(subject.ToString(), "10.3.0.127");
subject.first.port = Common::swap16(80);
EXPECT_EQ(subject.ToString(), "10.3.0.127:80");
subject.last.port = Common::swap16(88);
EXPECT_EQ(subject.ToString(), "10.3.0.127:80-88");
subject.last.ip_address = {10, 3, 0, 128};
EXPECT_EQ(subject.ToString(), "10.3.0.127-128:80-88");
subject.last.ip_address = {10, 3, 7, 35};
EXPECT_EQ(subject.ToString(), "10.3.0.127-10.3.7.35:80-88");
subject.first.ip_address = {192, 168, 0, 0};
subject.last.ip_address = {192, 168, 0, 255};
EXPECT_EQ(subject.ToString(), "192.168.0.0/24:80-88");
subject.last.ip_address = {192, 168, 3, 255};
EXPECT_EQ(subject.ToString(), "192.168.0.0/22:80-88");
subject.first.ip_address = {};
subject.last.ip_address = {255, 255, 255, 255};
EXPECT_EQ(subject.ToString(), "0.0.0.0/0:80-88");
}
TEST(StringUtil, CharacterEncodingConversion)
{
// wstring