summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJordan Woyak <jordan.woyak@gmail.com>2026-02-08 11:37:38 -0600
committerJordan Woyak <jordan.woyak@gmail.com>2026-02-15 20:14:15 -0600
commitd893565b781a0a033ae2ff12962df436de695343 (patch)
treec71f09f720d5e8972677d4936381a820bd22e80c
parentcf8573421cf67359c870915fbbbe967239e36aa0 (diff)
AMMediaboard: IP override improvements and other cleanups.
Overrides now also apply in reverse for accepted connections. IP Override UI now includes a description. Mario Kart Arcade GP 4x Multicabinet now works on one PC without any tricky IP configurations. Added AMMBCommandBind, NetDIMMBind, and some helper functions. Removed redundant settings.
-rw-r--r--Source/Core/Core/Config/MainSettings.cpp36
-rw-r--r--Source/Core/Core/Config/MainSettings.h1
-rw-r--r--Source/Core/Core/HW/DVD/AMMediaboard.cpp470
-rw-r--r--Source/Core/Core/HW/DVD/AMMediaboard.h9
-rw-r--r--Source/Core/DolphinQt/Settings/TriforceBaseboardSettingsDialog.cpp82
-rw-r--r--Source/Core/DolphinQt/Settings/TriforceBaseboardSettingsDialog.h2
6 files changed, 377 insertions, 223 deletions
diff --git a/Source/Core/Core/Config/MainSettings.cpp b/Source/Core/Core/Config/MainSettings.cpp
index 6edcccf186..bc749b4b37 100644
--- a/Source/Core/Core/Config/MainSettings.cpp
+++ b/Source/Core/Core/Config/MainSettings.cpp
@@ -645,26 +645,40 @@ const std::array<Info<s16>, EMULATED_LOGITECH_MIC_COUNT> MAIN_LOGITECH_MIC_VOLUM
static std::string GetDefaultTriforceIPOverrides()
{
+#if defined(ANDROID)
+ return "0.0.0.0/0=127.0.0.1";
+#else
constexpr std::string_view entries[] = {
- // CyCraft Connect IP
- "192.168.11.111=127.0.0.1",
- // Key of Avalon
- // Note: The server and client can't run on the same system,
- // but it's replaced with 127.0.0.1 here just for reference.
- "192.168.13.1=127.0.0.1",
+ // 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",
- // NAMCO Camera
- "192.168.29.104-108=127.0.0.1",
+ // 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",
- // All.Net Connect IP
- "192.168.150.16=127.0.0.1",
+ // 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_BIND_IP{{System::Main, "Core", "TriforceBindIP"}, "0.0.0.0"};
const Info<std::string> MAIN_TRIFORCE_IP_OVERRIDES{{System::Main, "Core", "TriforceIPOverrides"},
GetDefaultTriforceIPOverrides()};
diff --git a/Source/Core/Core/Config/MainSettings.h b/Source/Core/Core/Config/MainSettings.h
index ad28ca9119..dfbf166758 100644
--- a/Source/Core/Core/Config/MainSettings.h
+++ b/Source/Core/Core/Config/MainSettings.h
@@ -386,7 +386,6 @@ 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_BIND_IP;
extern const Info<std::string> MAIN_TRIFORCE_IP_OVERRIDES;
// GameCube path utility functions
diff --git a/Source/Core/Core/HW/DVD/AMMediaboard.cpp b/Source/Core/Core/HW/DVD/AMMediaboard.cpp
index 2fd1897783..4a79a01252 100644
--- a/Source/Core/Core/HW/DVD/AMMediaboard.cpp
+++ b/Source/Core/Core/HW/DVD/AMMediaboard.cpp
@@ -5,6 +5,7 @@
#include <algorithm>
#include <bit>
+#include <random>
#include <ranges>
#include <string>
#include <unordered_map>
@@ -514,95 +515,64 @@ static int PlatformPoll(std::span<WSAPOLLFD> pfds, std::chrono::milliseconds tim
#endif
}
-static GuestSocket NetDIMMAccept(GuestSocket guest_socket, sockaddr* addr, socklen_t* len)
+std::optional<ParsedIPOverride> ParseIPOverride(std::string_view str)
{
- const auto host_socket = GetHostSocket(guest_socket);
- WSAPOLLFD pfds[1]{{.fd = host_socket, .events = POLLIN}};
-
- // FYI: Currently using a 0ms timeout to make accept calls always non-blocking.
- constexpr auto timeout = std::chrono::milliseconds{0};
-
- const int result = PlatformPoll(pfds, timeout);
-
- if (result > 0 && (pfds[0].revents & POLLIN) != 0)
- {
- const auto client_sock = accept_(host_socket, addr, len);
- if (client_sock == INVALID_GUEST_SOCKET)
- {
- ERROR_LOG_FMT(AMMEDIABOARD, "GC-AM: accept() failed in NetDIMMAccept ({})",
- Common::StrNetworkError());
- s_last_error = SOCKET_ERROR;
- return INVALID_GUEST_SOCKET;
- }
- s_last_error = SSC_SUCCESS;
- return client_sock;
- }
-
- if (result == 0)
- {
- // Timeout
- s_last_error = SSC_EWOULDBLOCK;
- }
- else
- {
- ERROR_LOG_FMT(AMMEDIABOARD, "GC-AM: poll() failed in NetDIMMAccept ({})",
- Common::StrNetworkError());
- s_last_error = SOCKET_ERROR;
- }
- return INVALID_GUEST_SOCKET;
-}
-
-std::optional<std::pair<std::string_view, std::string_view>> ParseIPOverride(std::string_view str)
-{
- // Ignore everything after a space. Future proofing to allow for a comment/description string.
+ // Everything after a space is the description.
const auto ip_pair_str = std::string_view{str.begin(), std::ranges::find(str, ' ')};
const auto parts = SplitStringIntoArray<2>(ip_pair_str, '=');
- if (parts.has_value())
- return std::make_pair((*parts)[0], (*parts)[1]);
+ if (!parts.has_value())
+ return std::nullopt;
- return std::nullopt;
+ const bool have_description = ip_pair_str.size() != str.size();
+
+ return ParsedIPOverride{
+ .original = (*parts)[0],
+ .replacement = (*parts)[1],
+ .description = have_description ? str.substr(ip_pair_str.size() + 1) : std::string_view{},
+ };
}
struct IPAddressOverride
{
- Common::IPv4PortRange match;
+ Common::IPv4PortRange original;
Common::IPv4PortRange replacement;
// Caller should check if it matches first!
Common::IPv4Port ApplyOverride(Common::IPv4Port subject) const
{
+ // This logic could probably be better.
+ // Ranges of different sizes will be weird in general.
+
const auto replacement_first_ip_u32 = replacement.first.GetIPAddressValue();
const auto ip_count = 1u + u64(replacement.last.GetIPAddressValue()) - replacement_first_ip_u32;
const auto result_ip =
u32(replacement_first_ip_u32 +
- ((subject.GetIPAddressValue() - match.first.GetIPAddressValue()) % ip_count));
+ ((subject.GetIPAddressValue() - original.first.GetIPAddressValue()) % ip_count));
- Common::IPv4Port result{
- .ip_address = std::bit_cast<Common::IPAddress>(Common::BigEndianValue(result_ip)),
- };
+ subject.ip_address = std::bit_cast<Common::IPAddress>(Common::BigEndianValue{result_ip});
const auto replacement_first_port_u16 = replacement.first.GetPortValue();
const auto port_count = 1u + u32(replacement.last.GetPortValue()) - replacement_first_port_u16;
- if (port_count == 65536)
- {
- // If the replacement includes all ports then don't alter the port.
- // This allows "1.1.1.1:80=2.2.2.2" to do the obvious thing.
- // This logic could probably be better.
- // Ranges of different sizes will be weird in general.
-
- result.port = subject.port;
- }
- else
+ // If the replacement includes all ports then we don't alter the port.
+ // This allows "10.0.0.1:80-88=10.0.0.2" to have the expected behavior.
+ if (port_count != 65536u)
{
- const auto result_port =
+ const auto result_port_u16 =
u16(replacement_first_port_u16 +
- ((subject.GetPortValue() - match.first.GetPortValue()) % port_count));
- result.port = std::bit_cast<u16>(Common::BigEndianValue(result_port));
+ ((subject.GetPortValue() - original.first.GetPortValue()) % port_count));
+ subject.port = std::bit_cast<u16>(Common::BigEndianValue{result_port_u16});
}
- return result;
+ return subject;
+ }
+
+ Common::IPv4Port ReverseOverride(Common::IPv4Port subject) const
+ {
+ // Low effort implementation..
+ return IPAddressOverride{.original = replacement, .replacement = original}.ApplyOverride(
+ subject);
}
};
@@ -618,12 +588,12 @@ static IPOverrides GetIPOverrides()
const auto parts = ParseIPOverride(ip_pair_str);
if (parts.has_value())
{
- const auto match = Common::StringToIPv4PortRange(parts->first);
- const auto replacement = Common::StringToIPv4PortRange(parts->second);
+ const auto original = Common::StringToIPv4PortRange(parts->original);
+ const auto replacement = Common::StringToIPv4PortRange(parts->replacement);
- if (match.has_value() && replacement.has_value())
+ if (original.has_value() && replacement.has_value())
{
- result.emplace_back(*match, *replacement);
+ result.emplace_back(*original, *replacement);
continue;
}
@@ -634,18 +604,62 @@ static IPOverrides GetIPOverrides()
return result;
}
-static std::optional<Common::IPv4Port> GetAdjustedIPv4PortFromConfig(Common::IPv4Port subject)
+static std::optional<Common::IPv4Port> AdjustIPv4PortFromConfig(Common::IPv4Port subject)
{
// TODO: We should parse this elsewhere to avoid repeated string manipulations.
for (auto&& override : GetIPOverrides())
{
- if (override.match.IsMatch(subject))
+ if (override.original.IsMatch(subject))
return override.ApplyOverride(subject);
}
return std::nullopt;
}
+static std::optional<Common::IPv4Port> ReverseAdjustIPv4PortFromConfig(Common::IPv4Port subject)
+{
+ // TODO: We should parse this elsewhere to avoid repeated string manipulations.
+ for (auto&& override : GetIPOverrides())
+ {
+ if (override.replacement.IsMatch(subject))
+ return override.ReverseOverride(subject);
+ }
+
+ return std::nullopt;
+}
+
+// Ports are in host byte order.
+static bool BindEphemeralPort(SOCKET host_socket, Common::IPAddress ip_address,
+ u16 first_port_value, u16 last_port_value, u32 attempt_count)
+{
+ std::mt19937 rng(u32(Clock::now().time_since_epoch().count()));
+ std::uniform_int_distribution<u16> port_distribution{first_port_value, last_port_value};
+
+ sockaddr_in addr = {
+ .sin_family = AF_INET,
+ .sin_addr = std::bit_cast<in_addr>(ip_address),
+ };
+
+ while (attempt_count-- != 0)
+ {
+ const u16 port_value = port_distribution(rng);
+
+ addr.sin_port = htons(port_value);
+
+ const auto bind_result = bind(host_socket, reinterpret_cast<sockaddr*>(&addr), sizeof(addr));
+ const int bind_err = WSAGetLastError();
+
+ INFO_LOG_FMT(AMMEDIABOARD, "BindEphemeralPort: bind ({}:{}) = {} ({})",
+ Common::IPAddressToString(ip_address), port_value, bind_result,
+ Common::DecodeNetworkError(bind_err));
+
+ if (bind_result == 0)
+ return true;
+ }
+
+ return false;
+}
+
static s32 NetDIMMConnect(GuestSocket guest_socket, const GuestSocketAddress& guest_addr)
{
INFO_LOG_FMT(AMMEDIABOARD, "NetDIMMConnect: {}:{}",
@@ -655,12 +669,13 @@ static s32 NetDIMMConnect(GuestSocket guest_socket, const GuestSocketAddress& gu
.sin_family = guest_addr.ip_family,
};
- const auto adjusted_ipv4port =
- GetAdjustedIPv4PortFromConfig({guest_addr.ip_address, guest_addr.port});
+ // Adjust destination IP and port.
+ const auto adjusted_ipv4port = AdjustIPv4PortFromConfig({guest_addr.ip_address, guest_addr.port});
if (adjusted_ipv4port.has_value())
{
addr.sin_addr = std::bit_cast<in_addr>(adjusted_ipv4port->ip_address);
addr.sin_port = adjusted_ipv4port->port;
+
INFO_LOG_FMT(AMMEDIABOARD, "NetDIMMConnect: Overriding to: {}:{}",
Common::IPAddressToString(adjusted_ipv4port->ip_address),
ntohs(adjusted_ipv4port->port));
@@ -673,6 +688,34 @@ static s32 NetDIMMConnect(GuestSocket guest_socket, const GuestSocketAddress& gu
const auto host_socket = GetHostSocket(guest_socket);
+ // See if we have an override for the game modified IP.
+ // If so, adjust the source IP by binding the socket.
+ const auto adjusted_source_ipv4port = AdjustIPv4PortFromConfig({s_game_modified_ip_address, 0});
+ if (adjusted_source_ipv4port.has_value())
+ {
+ // FYI: We don't handle the situation if games bind outgoing TCP themselves.
+ // But I think that's unlikely.
+
+ const u16 first_port_value = adjusted_source_ipv4port->GetPortValue();
+
+ // If port zero is included then we don't care about the port number.
+ const bool use_any_port = first_port_value == 0;
+ const u32 attempt_count = use_any_port ? 1 : 10;
+
+ // TODO: Handle the range properly. AdjustIPv4PortFromConfig should return a port range.
+ // This magic 999 is here just to match our default config..
+ const u16 last_port_value = use_any_port ? 0 : first_port_value + 999;
+
+ const auto bind_result = BindEphemeralPort(host_socket, adjusted_source_ipv4port->ip_address,
+ first_port_value, last_port_value, attempt_count);
+
+ if (!bind_result)
+ {
+ s_last_error = SOCKET_ERROR;
+ return SOCKET_ERROR;
+ }
+ }
+
// Set socket to non-blocking
{
u_long val = 1;
@@ -752,6 +795,156 @@ static s32 NetDIMMConnect(GuestSocket guest_socket, const GuestSocketAddress& gu
return SOCKET_ERROR;
}
+static GuestSocket NetDIMMAccept(GuestSocket guest_socket, u8* guest_addr_ptr,
+ u8* guest_addrlen_ptr)
+{
+ // Either both parameters should be provided, or neither.
+ if ((guest_addr_ptr != nullptr) != (guest_addrlen_ptr != nullptr))
+ {
+ ERROR_LOG_FMT(AMMEDIABOARD_NET, "NetDIMMAccept: bad parmeters");
+
+ // TODO: Not hardware tested.
+ s_last_error = SSC_EFAULT;
+ return INVALID_GUEST_SOCKET;
+ }
+
+ const auto host_socket = GetHostSocket(guest_socket);
+ WSAPOLLFD pfds[1]{{.fd = host_socket, .events = POLLIN}};
+
+ // FYI: Currently using a 0ms timeout to make accept calls always non-blocking.
+ constexpr auto timeout = std::chrono::milliseconds{0};
+
+ DEBUG_LOG_FMT(AMMEDIABOARD, "NetDIMMAccept: {}({})", host_socket, int(guest_socket));
+
+ const int poll_result = PlatformPoll(pfds, timeout);
+
+ if (poll_result < 0) [[unlikely]]
+ {
+ // Poll failure.
+ ERROR_LOG_FMT(AMMEDIABOARD, "NetDIMMAccept: PlatformPoll: {}", Common::StrNetworkError());
+
+ s_last_error = SOCKET_ERROR;
+ return INVALID_GUEST_SOCKET;
+ }
+
+ if ((pfds[0].revents & POLLIN) == 0)
+ {
+ // Timeout.
+ DEBUG_LOG_FMT(AMMEDIABOARD, "NetDIMMAccept: Timeout.");
+
+ s_last_error = SSC_EWOULDBLOCK;
+ return INVALID_GUEST_SOCKET;
+ }
+
+ sockaddr_in addr;
+ socklen_t addrlen = sizeof(addr);
+ const auto client_sock = accept_(host_socket, reinterpret_cast<sockaddr*>(&addr), &addrlen);
+
+ if (client_sock == INVALID_GUEST_SOCKET)
+ {
+ ERROR_LOG_FMT(AMMEDIABOARD, "AMMBCommandAccept: accept: ({})", Common::StrNetworkError());
+ s_last_error = SOCKET_ERROR;
+ return INVALID_GUEST_SOCKET;
+ }
+
+ s_last_error = SSC_SUCCESS;
+
+ NOTICE_LOG_FMT(AMMEDIABOARD, "AMMBCommandAccept: {}:{}",
+ Common::IPAddressToString(std::bit_cast<Common::IPAddress>(addr.sin_addr)),
+ ntohs(addr.sin_port));
+
+ if (guest_addr_ptr == nullptr)
+ return client_sock;
+
+ GuestSocketAddress guest_addr{
+ .ip_family = u8(addr.sin_family),
+ .port = addr.sin_port,
+ .ip_address = std::bit_cast<Common::IPAddress>(addr.sin_addr),
+ };
+
+ if (const auto adjusted_ipv4port =
+ ReverseAdjustIPv4PortFromConfig({guest_addr.ip_address, guest_addr.port}))
+ {
+ guest_addr.ip_address = adjusted_ipv4port->ip_address;
+ guest_addr.port = adjusted_ipv4port->port;
+
+ NOTICE_LOG_FMT(AMMEDIABOARD, "AMMBCommandAccept: Translating result to: {}:{}",
+ Common::IPAddressToString(guest_addr.ip_address), ntohs(guest_addr.port));
+ }
+
+ const auto write_size =
+ std::min<u32>(Common::BitCastPtr<u32>(guest_addrlen_ptr), sizeof(guest_addr));
+
+ // Write out the addr.
+ std::memcpy(guest_addr_ptr, &guest_addr, write_size);
+
+ // Write out the addrlen.
+ *guest_addrlen_ptr = sizeof(guest_addr);
+
+ return client_sock;
+}
+
+static Common::IPv4Port GetAdjustedBindIPv4Port(Common::IPv4Port socket_addr)
+{
+ auto considered_ipv4 = socket_addr;
+
+ if (std::bit_cast<u32>(considered_ipv4.ip_address) == INADDR_ANY)
+ {
+ // Because the game is binding to "0.0.0.0",
+ // use the "game modified" IP for override purposes.
+ // If no override applies, then we still bind "0.0.0.0".
+ considered_ipv4.ip_address = s_game_modified_ip_address;
+ INFO_LOG_FMT(AMMEDIABOARD, "GetAdjustedBindIPv4Port: Considering game modified IP: {}",
+ Common::IPAddressToString(s_game_modified_ip_address));
+ }
+
+ if (const auto adjusted_ipv4 = AdjustIPv4PortFromConfig(considered_ipv4))
+ {
+ socket_addr = *adjusted_ipv4;
+ INFO_LOG_FMT(AMMEDIABOARD, "GetAdjustedBindIPv4Port: Overriding to: {}:{}",
+ Common::IPAddressToString(socket_addr.ip_address), ntohs(socket_addr.port));
+ }
+
+ return socket_addr;
+}
+
+static u32 NetDIMMBind(GuestSocket guest_socket, const GuestSocketAddress& guest_addr)
+{
+ const auto host_socket = GetHostSocket(guest_socket);
+
+ NOTICE_LOG_FMT(AMMEDIABOARD, "NetDIMMBind: {}({}) {}, {}, {}:{}", host_socket, int(guest_socket),
+ guest_addr.unknown_value, guest_addr.ip_family,
+ Common::IPAddressToString(guest_addr.ip_address), ntohs(guest_addr.port));
+
+ const auto adjusted_ipv4port = GetAdjustedBindIPv4Port({guest_addr.ip_address, guest_addr.port});
+
+ sockaddr_in addr{
+ .sin_family = guest_addr.ip_family,
+ .sin_port = adjusted_ipv4port.port,
+ .sin_addr = std::bit_cast<in_addr>(adjusted_ipv4port.ip_address),
+ };
+
+ const int bind_result = bind(host_socket, reinterpret_cast<const sockaddr*>(&addr), sizeof(addr));
+ const int err = WSAGetLastError();
+
+ INFO_LOG_FMT(AMMEDIABOARD_NET, "NetDIMMBind: bind( {}({}), ({},{}:{}) ):{}", host_socket,
+ u32(guest_socket), addr.sin_family,
+ Common::IPAddressToString(adjusted_ipv4port.ip_address),
+ Common::swap16(adjusted_ipv4port.port), bind_result);
+
+ if (bind_result < 0)
+ {
+ const auto* const err_msg = Common::DecodeNetworkError(err);
+ ERROR_LOG_FMT(AMMEDIABOARD, "NetDIMMBind bind() = {} ({})", err, err_msg);
+
+ PanicAlertFmt("Failed to bind socket {}:{}\nError: {} ({})",
+ Common::IPAddressToString(adjusted_ipv4port.ip_address),
+ ntohs(adjusted_ipv4port.port), err, err_msg);
+ }
+
+ return bind_result;
+}
+
static void AMMBCommandRecv(u32 parameter_offset, u32 network_buffer_base)
{
const auto fd = GetHostSocket(GuestSocket(s_media_buffer_32[parameter_offset]));
@@ -873,43 +1066,44 @@ static void AMMBCommandAccept(u32 parameter_offset, u32 network_buffer_base)
const u32 addr_off = s_media_buffer_32[parameter_offset + 1];
const u32 addrlen_off = s_media_buffer_32[parameter_offset + 2];
- u32 ret{};
+ auto* const addrlen_ptr =
+ GetSafePtr(s_network_command_buffer, network_buffer_base, addrlen_off, sizeof(u32));
- // Either both parameters should be provided, or neither.
- if ((addr_off != 0) != (addrlen_off != 0))
- {
- WARN_LOG_FMT(AMMEDIABOARD_NET, "AMMBCommandAccept: Unexpected parameters: {}, {}, {}",
- u32(guest_socket), addr_off, addrlen_off);
+ auto* const addr_ptr = (addrlen_ptr == nullptr) ?
+ nullptr :
+ GetSafePtr(s_network_command_buffer, network_buffer_base, addr_off,
+ Common::BitCastPtr<u32>(addrlen_ptr));
- // TODO: Not hardware tested.
- s_last_error = SSC_EFAULT;
- ret = SOCKET_ERROR;
- }
- else
- {
- sockaddr addr;
- socklen_t addrlen = sizeof(addr);
- ret = u32(NetDIMMAccept(guest_socket, &addr, &addrlen));
+ const auto accept_result = NetDIMMAccept(guest_socket, addr_ptr, addrlen_ptr);
- INFO_LOG_FMT(AMMEDIABOARD_NET, "GC-AM: accept( {} ):{}", u32(guest_socket), int(ret));
+ s_media_buffer_32[1] = u32(accept_result);
+}
- auto* const addrlen_ptr =
- GetSafePtr(s_network_command_buffer, network_buffer_base, addrlen_off, sizeof(u32));
- if (addrlen_ptr != nullptr)
- {
- // Read the buffer size.
- addrlen = std::min<socklen_t>(addrlen, Common::BitCastPtr<u32>(addrlen_ptr));
- // Write out the proper length.
- Common::BitCastPtr<u32>(addrlen_ptr) = sizeof(addr);
-
- auto* const addr_ptr =
- GetSafePtr(s_network_command_buffer, network_buffer_base, addr_off, addrlen);
- if (addr_ptr != nullptr)
- memcpy(addr_ptr, &addr, addrlen);
- }
+static void AMMBCommandBind()
+{
+ const auto guest_socket = GuestSocket(s_media_buffer_32[2]);
+ const u32 addr_offset = s_media_buffer_32[3];
+ const u32 len = s_media_buffer_32[4];
+
+ GuestSocketAddress guest_addr;
+
+ if (len != sizeof(guest_addr))
+ {
+ ERROR_LOG_FMT(AMMEDIABOARD_NET, "AMMBCommandBind: Unexpected length: {}", len);
+ return;
}
- s_media_buffer_32[1] = ret;
+ const auto* addr_ptr =
+ GetSafePtr(s_network_command_buffer, NetworkCommandAddress2, addr_offset, sizeof(guest_addr));
+ if (addr_ptr == nullptr)
+ return;
+
+ memcpy(&guest_addr, addr_ptr, sizeof(guest_addr));
+
+ const auto bind_result = NetDIMMBind(guest_socket, guest_addr);
+
+ s_media_buffer_32[1] = bind_result;
+ s_last_error = SSC_SUCCESS;
}
// Expects a pointer to a GuestFdSet or nullptr.
@@ -1315,80 +1509,8 @@ u32 ExecuteCommand(std::array<u32, 3>& dicmd_buf, u32* diimm_buf, u32 address, u
AMMBCommandAccept(2, NetworkCommandAddress2);
break;
case AMMBCommand::Bind:
- {
- const auto guest_socket = GuestSocket(s_media_buffer_32[2]);
- const auto host_socket = GetHostSocket(guest_socket);
- const u32 addr_offset = s_media_buffer_32[3];
- const u32 len = s_media_buffer_32[4];
-
- GuestSocketAddress guest_addr;
-
- if (len != sizeof(guest_addr))
- {
- ERROR_LOG_FMT(AMMEDIABOARD_NET, "AMMBCommand::Bind: Unexpected length: {}", len);
- break;
- }
-
- const auto* addr_ptr = GetSafePtr(s_network_command_buffer, NetworkCommandAddress2,
- addr_offset, sizeof(guest_addr));
- if (addr_ptr == nullptr)
- break;
-
- memcpy(&guest_addr, addr_ptr, sizeof(guest_addr));
-
- // Triforce titles typically rely on hardcoded IP addresses.
- // Our config allows this to be overriden.
-
- NOTICE_LOG_FMT(AMMEDIABOARD, "AMMBCommand::Bind: {}, {}, {}:{}", guest_addr.struct_size,
- guest_addr.ip_family, Common::IPAddressToString(guest_addr.ip_address),
- ntohs(guest_addr.port));
-
- // Apply "BindIP" if it's valid.
- const auto override_bind_ip = inet_addr(Config::Get(Config::MAIN_TRIFORCE_BIND_IP).c_str());
- if (override_bind_ip != INADDR_NONE)
- {
- guest_addr.ip_address = std::bit_cast<Common::IPAddress>(override_bind_ip);
- INFO_LOG_FMT(AMMEDIABOARD, "AMMBCommand::Bind: Overriding IP to: {}",
- Common::IPAddressToString(guest_addr.ip_address));
- }
-
- // Apply "IPOverrides" in case config wants ports adjusted.
- const auto adjusted_ipv4port =
- GetAdjustedIPv4PortFromConfig({guest_addr.ip_address, guest_addr.port});
- if (adjusted_ipv4port.has_value())
- {
- guest_addr.ip_address = adjusted_ipv4port->ip_address;
- guest_addr.port = adjusted_ipv4port->port;
- INFO_LOG_FMT(AMMEDIABOARD, "AMMBCommand::Bind: Overriding to: {}:{}",
- Common::IPAddressToString(guest_addr.ip_address), ntohs(guest_addr.port));
- }
-
- sockaddr_in addr{
- .sin_family = guest_addr.ip_family,
- .sin_port = guest_addr.port,
- .sin_addr = std::bit_cast<in_addr>(guest_addr.ip_address),
- };
-
- const int bind_result =
- bind(host_socket, reinterpret_cast<const sockaddr*>(&addr), sizeof(addr));
- const int err = WSAGetLastError();
-
- INFO_LOG_FMT(AMMEDIABOARD_NET, "GC-AM: bind( {}({}), ({},{}:{}) ):{} ({})", host_socket,
- u32(guest_socket), addr.sin_family, inet_ntoa(addr.sin_addr),
- Common::swap16(addr.sin_port), bind_result, err);
-
- if (bind_result < 0)
- {
- const auto err_msg = Common::DecodeNetworkError(err);
- PanicAlertFmt("Failed to bind socket (error {}: {})", err, err_msg);
- ERROR_LOG_FMT(AMMEDIABOARD, "GC-AM: AMMBCommand::Bind failed, bind() = {} ({})", err,
- err_msg);
- }
-
- s_media_buffer_32[1] = bind_result;
- s_last_error = SSC_SUCCESS;
+ AMMBCommandBind();
break;
- }
case AMMBCommand::Closesocket:
AMMBCommandClosesocket(2);
break;
diff --git a/Source/Core/Core/HW/DVD/AMMediaboard.h b/Source/Core/Core/HW/DVD/AMMediaboard.h
index 5079a7fdd3..937d2b9228 100644
--- a/Source/Core/Core/HW/DVD/AMMediaboard.h
+++ b/Source/Core/Core/HW/DVD/AMMediaboard.h
@@ -7,7 +7,6 @@
#include <optional>
#include <span>
#include <string_view>
-#include <utility>
#include "Common/ChunkFile.h"
#include "Common/CommonTypes.h"
@@ -243,6 +242,12 @@ bool GetTestMenu();
void Shutdown();
void DoState(PointerWrap& p);
-std::optional<std::pair<std::string_view, std::string_view>> ParseIPOverride(std::string_view str);
+struct ParsedIPOverride
+{
+ std::string_view original;
+ std::string_view replacement;
+ std::string_view description;
+};
+std::optional<ParsedIPOverride> ParseIPOverride(std::string_view str);
}; // namespace AMMediaboard
diff --git a/Source/Core/DolphinQt/Settings/TriforceBaseboardSettingsDialog.cpp b/Source/Core/DolphinQt/Settings/TriforceBaseboardSettingsDialog.cpp
index 23d300dc43..5ae697c148 100644
--- a/Source/Core/DolphinQt/Settings/TriforceBaseboardSettingsDialog.cpp
+++ b/Source/Core/DolphinQt/Settings/TriforceBaseboardSettingsDialog.cpp
@@ -4,7 +4,6 @@
#include "DolphinQt/Settings/TriforceBaseboardSettingsDialog.h"
#include <QDialogButtonBox>
-#include <QFormLayout>
#include <QGroupBox>
#include <QHeaderView>
#include <QLineEdit>
@@ -22,30 +21,28 @@
#include "DolphinQt/QtUtils/QtUtils.h"
+static constexpr int COLUMN_EMULATED = 0;
+static constexpr int COLUMN_REAL = 1;
+static constexpr int COLUMN_DESCRIPTION = 2;
+
TriforceBaseboardSettingsDialog::TriforceBaseboardSettingsDialog(QWidget* parent) : QDialog{parent}
{
setWindowTitle(tr("Triforce Baseboard"));
- connect(this, &TriforceBaseboardSettingsDialog::accepted, this,
- &TriforceBaseboardSettingsDialog::SaveConfig);
-
auto* const dialog_layout = new QVBoxLayout{this};
auto* const ip_override_group = new QGroupBox{tr("IP Address Overrides")};
dialog_layout->addWidget(ip_override_group);
- auto* const ip_override_layout = new QFormLayout{ip_override_group};
-
- m_bind_ip_edit = new QLineEdit;
- ip_override_layout->addRow(tr("Bind IP: "), m_bind_ip_edit);
+ auto* const ip_override_layout = new QVBoxLayout{ip_override_group};
m_ip_overrides_table = new QTableWidget;
- ip_override_layout->addRow(tr("IP Overrides: "), m_ip_overrides_table);
+ ip_override_layout->addWidget(m_ip_overrides_table);
+ m_ip_overrides_table->setColumnCount(3);
+ m_ip_overrides_table->setHorizontalHeaderLabels({tr("Emulated"), tr("Real"), tr("Description")});
m_ip_overrides_table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
-
- m_ip_overrides_table->setColumnCount(2);
- m_ip_overrides_table->setHorizontalHeaderLabels({tr("Match"), tr("Replacement")});
+ m_ip_overrides_table->setSizeAdjustPolicy(QTableWidget::AdjustToContents);
m_ip_overrides_table->setEditTriggers(QAbstractItemView::DoubleClicked |
QAbstractItemView::EditKeyPressed);
@@ -57,11 +54,18 @@ TriforceBaseboardSettingsDialog::TriforceBaseboardSettingsDialog(QWidget* parent
connect(load_default_button, &QPushButton::clicked, this,
&TriforceBaseboardSettingsDialog::LoadDefault);
+ auto* const clear_button = new QPushButton(tr("Clear"));
+ connect(clear_button, &QPushButton::clicked, this, &TriforceBaseboardSettingsDialog::OnClear);
+
button_box->addButton(load_default_button, QDialogButtonBox::ActionRole);
+ button_box->addButton(clear_button, QDialogButtonBox::ActionRole);
connect(button_box, &QDialogButtonBox::accepted, this, &TriforceBaseboardSettingsDialog::accept);
connect(button_box, &QDialogButtonBox::rejected, this, &TriforceBaseboardSettingsDialog::reject);
+ connect(this, &TriforceBaseboardSettingsDialog::accepted, this,
+ &TriforceBaseboardSettingsDialog::SaveConfig);
+
connect(m_ip_overrides_table, &QTableWidget::itemChanged, this, [this](QTableWidgetItem* item) {
const int row_count = m_ip_overrides_table->rowCount();
const bool is_last_row = item->row() == row_count - 1;
@@ -70,12 +74,11 @@ TriforceBaseboardSettingsDialog::TriforceBaseboardSettingsDialog(QWidget* parent
if (!item->text().isEmpty())
m_ip_overrides_table->insertRow(row_count);
}
- else
+ 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.
- if (item->text().isEmpty())
- m_ip_overrides_table->removeRow(item->row());
+ m_ip_overrides_table->removeRow(item->row());
}
});
@@ -86,8 +89,6 @@ TriforceBaseboardSettingsDialog::TriforceBaseboardSettingsDialog(QWidget* parent
void TriforceBaseboardSettingsDialog::LoadConfig()
{
- m_bind_ip_edit->setText(QString::fromUtf8(Config::Get(Config::MAIN_TRIFORCE_BIND_IP)));
-
m_ip_overrides_table->setRowCount(0);
// Add the final empty row.
@@ -99,8 +100,8 @@ void TriforceBaseboardSettingsDialog::LoadConfig()
for (auto&& ip_pair : ip_overrides_str | std::views::split(','))
{
const auto ip_pair_str = std::string_view{ip_pair};
- const auto parts = AMMediaboard::ParseIPOverride(ip_pair_str);
- if (!parts.has_value())
+ const auto parsed = AMMediaboard::ParseIPOverride(ip_pair_str);
+ if (!parsed.has_value())
{
ERROR_LOG_FMT(COMMON, "Bad IP pair string: {}", ip_pair_str);
continue;
@@ -108,8 +109,12 @@ void TriforceBaseboardSettingsDialog::LoadConfig()
m_ip_overrides_table->insertRow(row);
- m_ip_overrides_table->setItem(row, 0, new QTableWidgetItem(QString::fromUtf8(parts->first)));
- m_ip_overrides_table->setItem(row, 1, new QTableWidgetItem(QString::fromUtf8(parts->second)));
+ m_ip_overrides_table->setItem(row, COLUMN_EMULATED,
+ new QTableWidgetItem(QString::fromUtf8(parsed->original)));
+ m_ip_overrides_table->setItem(row, COLUMN_REAL,
+ new QTableWidgetItem(QString::fromUtf8(parsed->replacement)));
+ m_ip_overrides_table->setItem(row, COLUMN_DESCRIPTION,
+ new QTableWidgetItem(QString::fromUtf8(parsed->description)));
++row;
}
@@ -117,28 +122,31 @@ void TriforceBaseboardSettingsDialog::LoadConfig()
void TriforceBaseboardSettingsDialog::SaveConfig()
{
- Config::SetBaseOrCurrent(Config::MAIN_TRIFORCE_BIND_IP, m_bind_ip_edit->text().toStdString());
-
// Ignore our empty row.
const int row_count = m_ip_overrides_table->rowCount() - 1;
std::vector<std::string> replacements(row_count);
for (int row = 0; row < row_count; ++row)
{
+ auto* const original_item = m_ip_overrides_table->item(row, COLUMN_EMULATED);
+ auto* const replacement_item = m_ip_overrides_table->item(row, COLUMN_REAL);
+
// Skip incomplete rows.
- if (m_ip_overrides_table->item(row, 0) == nullptr ||
- m_ip_overrides_table->item(row, 1) == nullptr)
- {
+ if (original_item == nullptr || replacement_item == nullptr)
continue;
- }
- replacements[row] =
- std::string(StripWhitespace(m_ip_overrides_table->item(row, 0)->text().toStdString())) +
- "=" +
- std::string(StripWhitespace(m_ip_overrides_table->item(row, 1)->text().toStdString()));
- }
+ replacements[row] = fmt::format("{}={}", StripWhitespace(original_item->text().toStdString()),
+ replacement_item->text().toStdString());
+
+ auto* const description_item = m_ip_overrides_table->item(row, COLUMN_DESCRIPTION);
+ if (description_item == nullptr)
+ continue;
+
+ const auto description = description_item->text().toStdString();
- // TODO: Ideally we'd remove duplicates based on the first IP.
+ if (!description.empty())
+ (replacements[row] += ' ') += description;
+ }
Config::SetBaseOrCurrent(Config::MAIN_TRIFORCE_IP_OVERRIDES,
fmt::format("{}", fmt::join(replacements, ",")));
@@ -147,8 +155,14 @@ void TriforceBaseboardSettingsDialog::SaveConfig()
void TriforceBaseboardSettingsDialog::LoadDefault()
{
// This alters the config before "OK" is pressed. Bad UX..
- Config::SetBaseOrCurrent(Config::MAIN_TRIFORCE_BIND_IP, Config::DefaultState{});
+
Config::SetBaseOrCurrent(Config::MAIN_TRIFORCE_IP_OVERRIDES, Config::DefaultState{});
LoadConfig();
}
+
+void TriforceBaseboardSettingsDialog::OnClear()
+{
+ Config::SetBaseOrCurrent(Config::MAIN_TRIFORCE_IP_OVERRIDES, "");
+ LoadConfig();
+}
diff --git a/Source/Core/DolphinQt/Settings/TriforceBaseboardSettingsDialog.h b/Source/Core/DolphinQt/Settings/TriforceBaseboardSettingsDialog.h
index e472739f71..f57d562da1 100644
--- a/Source/Core/DolphinQt/Settings/TriforceBaseboardSettingsDialog.h
+++ b/Source/Core/DolphinQt/Settings/TriforceBaseboardSettingsDialog.h
@@ -20,7 +20,7 @@ private:
void SaveConfig();
void LoadDefault();
+ void OnClear();
- QLineEdit* m_bind_ip_edit;
QTableWidget* m_ip_overrides_table;
};