diff --git a/src/common/emulatorConfig.cpp b/src/common/emulatorConfig.cpp
index 9dc6aefce..5470bce62 100644
--- a/src/common/emulatorConfig.cpp
+++ b/src/common/emulatorConfig.cpp
@@ -100,6 +100,10 @@ bool VulkanValidationEnabled() {
return g_config->vulkan_validation_enabled;
}
+bool IsNetworkConnected() {
+ return g_config->network_connected;
+}
+
bool ShaderValidationEnabled() {
return g_config->shader_validation_enabled;
}
diff --git a/src/common/emulatorConfig.h b/src/common/emulatorConfig.h
index 6cd047fc3..c7d641878 100644
--- a/src/common/emulatorConfig.h
+++ b/src/common/emulatorConfig.h
@@ -58,6 +58,7 @@ struct ConfigOptions {
bool splash_enabled = true;
bool vr_enabled = false;
bool amd_cpu_enabled = false;
+ bool network_connected = false;
uint32_t vblank_frequency = 60;
uint32_t console_language = DEFAULT_CONSOLE_LANGUAGE;
bool vulkan_validation_enabled = false;
@@ -105,6 +106,7 @@ bool HdrEnabled();
bool HideCursorEnabled();
bool VrEnabled();
bool AmdCpuEnabled();
+bool IsNetworkConnected();
uint32_t GetVblankFrequency();
uint32_t GetConsoleLanguage();
bool VulkanValidationEnabled();
diff --git a/src/launcher/forms/configuration_edit_dialog.ui b/src/launcher/forms/configuration_edit_dialog.ui
index 5c0baf00d..b79c83f0e 100644
--- a/src/launcher/forms/configuration_edit_dialog.ui
+++ b/src/launcher/forms/configuration_edit_dialog.ui
@@ -329,6 +329,23 @@
+
+ Network
+
+ -
+
+ Let games use your computer's local network connection.
+ Enable network connection
+
+
+ -
+
+ Qt::Vertical
+ 2040
+
+
+
+
Debug
diff --git a/src/launcher/include/configuration.h b/src/launcher/include/configuration.h
index 649c7cee3..e4bf3c8bf 100644
--- a/src/launcher/include/configuration.h
+++ b/src/launcher/include/configuration.h
@@ -124,6 +124,7 @@ public:
bool tessellation_enabled = false;
bool trophy_enabled = true;
bool skip_notice_screen = false;
+ bool network_connected = false;
int vblank_frequency = 60;
int console_language = DEFAULT_CONSOLE_LANGUAGE;
bool vulkan_validation_enabled = false;
@@ -160,6 +161,7 @@ public:
tessellation_enabled = other.tessellation_enabled;
trophy_enabled = other.trophy_enabled;
skip_notice_screen = other.skip_notice_screen;
+ network_connected = other.network_connected;
vblank_frequency = other.vblank_frequency;
console_language = other.console_language;
vulkan_validation_enabled = other.vulkan_validation_enabled;
diff --git a/src/launcher/src/configuration.cpp b/src/launcher/src/configuration.cpp
index 76bc81d7b..b2527f39e 100644
--- a/src/launcher/src/configuration.cpp
+++ b/src/launcher/src/configuration.cpp
@@ -17,6 +17,7 @@ QVariantMap Configuration::GameSettings() const {
KYTY_CFG_SET(tessellation_enabled);
KYTY_CFG_SET(trophy_enabled);
KYTY_CFG_SET(skip_notice_screen);
+ KYTY_CFG_SET(network_connected);
KYTY_CFG_SET(vblank_frequency);
KYTY_CFG_SET(console_language);
KYTY_CFG_SET(vulkan_validation_enabled);
@@ -63,6 +64,7 @@ void Configuration::ReadGameSettingsValues(const Settings& s) {
KYTY_CFG_GET(tessellation_enabled);
trophy_enabled = s.value("trophy_enabled", trophy_enabled).toBool();
KYTY_CFG_GET(skip_notice_screen);
+ KYTY_CFG_GET(network_connected);
vblank_frequency = s.value("vblank_frequency", vblank_frequency).toInt();
console_language = s.value("console_language", console_language).toInt();
if (console_language < 0 || console_language > MAX_CONSOLE_LANGUAGE) {
diff --git a/src/launcher/src/configurationEditDialog.cpp b/src/launcher/src/configurationEditDialog.cpp
index 3493b00e2..4c95ef6a7 100644
--- a/src/launcher/src/configurationEditDialog.cpp
+++ b/src/launcher/src/configurationEditDialog.cpp
@@ -279,6 +279,7 @@ void ConfigurationEditDialog::Init(const Configuration& info) {
m_ui->checkBox_sync_raw_image_buffers->setChecked(info.sync_raw_image_buffers);
m_ui->checkBox_tessellation->setChecked(info.tessellation_enabled);
m_ui->checkBox_skip_notice_screen->setChecked(info.skip_notice_screen);
+ m_ui->checkBox_network_connected->setChecked(info.network_connected);
m_ui->checkBox_trophy_notifications->setChecked(info.trophy_enabled);
m_ui->spinBox_vblank_frequency->setValue(info.vblank_frequency);
m_ui->comboBox_console_language->clear();
@@ -441,6 +442,7 @@ static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui, boo
info.sync_raw_image_buffers = ui.checkBox_sync_raw_image_buffers->isChecked();
info.tessellation_enabled = ui.checkBox_tessellation->isChecked();
info.skip_notice_screen = ui.checkBox_skip_notice_screen->isChecked();
+ info.network_connected = ui.checkBox_network_connected->isChecked();
info.trophy_enabled = ui.checkBox_trophy_notifications->isChecked();
info.vblank_frequency = ui.spinBox_vblank_frequency->value();
info.console_language = ui.comboBox_console_language->currentIndex();
diff --git a/src/launcher/src/mainDialog.cpp b/src/launcher/src/mainDialog.cpp
index 8d3ba3da0..260999331 100644
--- a/src/launcher/src/mainDialog.cpp
+++ b/src/launcher/src/mainDialog.cpp
@@ -255,6 +255,7 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
args << "--sync-raw-image-buffers" << BoolArg(info.sync_raw_image_buffers);
args << "--trophy-notifications" << BoolArg(info.trophy_enabled);
args << "--skip-notice-screen" << BoolArg(info.skip_notice_screen);
+ args << "--network-connected" << BoolArg(info.network_connected);
if (info.tessellation_enabled) {
args << "--tessellation";
}
diff --git a/src/libs/network.cpp b/src/libs/network.cpp
index 847f53f2d..fc9688544 100644
--- a/src/libs/network.cpp
+++ b/src/libs/network.cpp
@@ -15,6 +15,9 @@
#else
#include
#include
+#include
+#include
+#include
#include
#include
#include
@@ -41,10 +44,12 @@
#include
#include
#include
+#include
#include
#include
#include
#include
+#include
#include
#include
#include
@@ -3386,7 +3391,6 @@ constexpr int NET_CTL_STATE_DISCONNECTED = 0;
constexpr int NET_CTL_STATE_IPOBTAINED = 3;
constexpr int NET_CTL_DEVICE_WIRED = 0;
constexpr int NET_CTL_DEVICE_WIRELESS = 1;
-constexpr int NET_CTL_LINK_DISCONNECTED = 0;
constexpr int NET_CTL_LINK_CONNECTED = 1;
constexpr int NET_CTL_EVENT_DISCONNECTED = 1;
constexpr int NET_CTL_EVENT_IPOBTAINED = 3;
@@ -3448,8 +3452,6 @@ struct NetCtlCallbackSlot {
static Common::Mutex g_net_ctl_callbacks_mutex;
static std::array g_net_ctl_callbacks;
-static std::atomic_bool g_net_ctl_connected = false;
-static std::atomic_bool g_net_ctl_status_initialized = false;
struct HostNetworkInfo {
bool connected = false;
@@ -3463,22 +3465,16 @@ struct HostNetworkInfo {
};
static void CopyIpv4String(char* dst, size_t dst_size, const sockaddr* addr) {
-#if defined(_WIN32)
if (dst == nullptr || dst_size == 0 || addr == nullptr || addr->sa_family != AF_INET) {
return;
}
const auto* in = reinterpret_cast(addr);
inet_ntop(AF_INET, &in->sin_addr, dst, static_cast(dst_size));
-#else
- (void)dst;
- (void)dst_size;
- (void)addr;
-#endif
}
-static void CopyIpv4Netmask(char* dst, size_t dst_size, uint8_t prefix_len) {
#if defined(_WIN32)
+static void CopyIpv4Netmask(char* dst, size_t dst_size, uint8_t prefix_len) {
if (dst == nullptr || dst_size == 0 || prefix_len > 32) {
return;
}
@@ -3487,26 +3483,22 @@ static void CopyIpv4Netmask(char* dst, size_t dst_size, uint8_t prefix_len) {
in_addr in {};
in.S_un.S_addr = htonl(mask);
inet_ntop(AF_INET, &in, dst, static_cast(dst_size));
-#else
- (void)dst;
- (void)dst_size;
- (void)prefix_len;
-#endif
}
+#endif
static HostNetworkInfo QueryHostNetworkInfo() {
HostNetworkInfo info;
#if defined(_WIN32)
- ULONG flags = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST;
+ ULONG flags = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_INCLUDE_GATEWAYS;
ULONG size = 0;
- auto ret = GetAdaptersAddresses(AF_UNSPEC, flags, nullptr, nullptr, &size);
+ auto ret = GetAdaptersAddresses(AF_INET, flags, nullptr, nullptr, &size);
if (ret != ERROR_BUFFER_OVERFLOW || size == 0) {
return info;
}
std::vector buffer(size);
auto* adapters = reinterpret_cast(buffer.data());
- ret = GetAdaptersAddresses(AF_UNSPEC, flags, nullptr, adapters, &size);
+ ret = GetAdaptersAddresses(AF_INET, flags, nullptr, adapters, &size);
if (ret != NO_ERROR) {
return info;
}
@@ -3557,40 +3549,76 @@ static HostNetworkInfo QueryHostNetworkInfo() {
}
return info;
}
- } else if (addr->sa_family == AF_INET6) {
- const auto* in6 = reinterpret_cast(addr);
- if (!IN6_IS_ADDR_LOOPBACK(&in6->sin6_addr) &&
- !IN6_IS_ADDR_UNSPECIFIED(&in6->sin6_addr)) {
- info.connected = true;
- }
}
}
}
return info;
#else
- info.connected = true;
+ ifaddrs* addresses = nullptr;
+ if (getifaddrs(&addresses) != 0) {
+ return info;
+ }
+ const auto usable = [](const ifaddrs* address) {
+ return address->ifa_addr != nullptr && address->ifa_addr->sa_family == AF_INET &&
+ (address->ifa_flags & (IFF_UP | IFF_RUNNING | IFF_LOOPBACK)) ==
+ (IFF_UP | IFF_RUNNING) &&
+ reinterpret_cast(address->ifa_addr)->sin_addr.s_addr != 0;
+ };
+ const ifaddrs* selected = nullptr;
+ for (auto* address = addresses; address != nullptr; address = address->ifa_next) {
+ if (usable(address)) {
+ selected = address;
+ break;
+ }
+ }
+ std::ifstream routes("/proc/net/route");
+ std::string line;
+ uint32_t best_metric = std::numeric_limits::max();
+ uint32_t gateway = 0;
+ bool has_default_route = false;
+ while (std::getline(routes, line)) {
+ std::istringstream fields(line);
+ std::string name;
+ uint32_t destination = 0, candidate_gateway = 0, flags = 0, refs = 0, uses = 0;
+ uint32_t metric = 0, mask = 0;
+ if (!(fields >> name >> std::hex >> destination >> candidate_gateway >> flags >>
+ std::dec >> refs >> uses >> metric >> std::hex >> mask) ||
+ destination != 0 || mask != 0 || (flags & (RTF_UP | RTF_REJECT)) != RTF_UP ||
+ (has_default_route && metric >= best_metric)) {
+ continue;
+ }
+ for (auto* address = addresses; address != nullptr; address = address->ifa_next) {
+ if (usable(address) && name == address->ifa_name) {
+ selected = address;
+ gateway = candidate_gateway;
+ best_metric = metric;
+ has_default_route = true;
+ break;
+ }
+ }
+ }
+ if (selected != nullptr) {
+ CopyIpv4String(info.ip_address, sizeof(info.ip_address), selected->ifa_addr);
+ CopyIpv4String(info.netmask, sizeof(info.netmask), selected->ifa_netmask);
+ info.connected = info.ip_address[0] != '\0';
+ if (has_default_route) {
+ in_addr address {gateway};
+ inet_ntop(AF_INET, &address, info.default_route, sizeof(info.default_route));
+ }
+ }
+ freeifaddrs(addresses);
return info;
#endif
}
static bool NetCtlConnected() {
- if (!g_net_ctl_status_initialized.load()) {
- g_net_ctl_connected = false;
- g_net_ctl_status_initialized = true;
- LOGF("\t host network connected = false (forced offline)\n");
- }
-
- return g_net_ctl_connected.load();
+ return Config::IsNetworkConnected() && QueryHostNetworkInfo().connected;
}
int KYTY_SYSV_ABI NetCtlInit() {
PRINT_NAME();
- g_net_ctl_connected = false;
- g_net_ctl_status_initialized = true;
- LOGF("\t host network connected = false (forced offline)\n");
-
return OK;
}
@@ -3651,7 +3679,7 @@ int KYTY_SYSV_ABI NetCtlGetStateV6(int* state) {
return NET_CTL_ERROR_INVALID_ADDR;
}
- *state = (NetCtlConnected() ? NET_CTL_STATE_IPOBTAINED : NET_CTL_STATE_DISCONNECTED);
+ *state = NET_CTL_STATE_DISCONNECTED;
return OK;
}
@@ -3720,24 +3748,28 @@ int KYTY_SYSV_ABI NetCtlGetResult(int event_type, int* error_code) {
int KYTY_SYSV_ABI NetCtlGetInfo(int code, NetCtlInfo* info) {
PRINT_NAME();
- EXIT_NOT_IMPLEMENTED(info == nullptr);
-
+ if (info == nullptr) {
+ return NET_CTL_ERROR_INVALID_ADDR;
+ }
+ if (code < 1 || code > 21) {
+ return NET_CTL_ERROR_INVALID_CODE;
+ }
+ if (!Config::IsNetworkConnected()) {
+ return NET_CTL_ERROR_NOT_CONNECTED;
+ }
+ const auto host = QueryHostNetworkInfo();
+ if (!host.connected || (code == 16 && host.default_route[0] == '\0')) {
+ return NET_CTL_ERROR_NOT_CONNECTED;
+ }
memset(info, 0, sizeof(NetCtlInfo));
- // Online/host-backed info responses are left below for testing, but disabled
- // to preserve the disconnected-console behavior.
- return NET_CTL_ERROR_NOT_CONNECTED;
-
switch (code) {
case 1:
- info->device =
- (QueryHostNetworkInfo().wireless ? NET_CTL_DEVICE_WIRELESS : NET_CTL_DEVICE_WIRED);
+ info->device = (host.wireless ? NET_CTL_DEVICE_WIRELESS : NET_CTL_DEVICE_WIRED);
break;
- case 2: info->ether_addr = QueryHostNetworkInfo().ether_addr; break;
+ case 2: info->ether_addr = host.ether_addr; break;
case 3: info->mtu = 1500; break;
- case 4:
- info->link = (NetCtlConnected() ? NET_CTL_LINK_CONNECTED : NET_CTL_LINK_DISCONNECTED);
- break;
+ case 4: info->link = NET_CTL_LINK_CONNECTED; break;
case 5: break;
case 6: break;
case 7: info->wifi_security = 0; break;
@@ -3748,28 +3780,25 @@ int KYTY_SYSV_ABI NetCtlGetInfo(int code, NetCtlInfo* info) {
case 12: break;
case 13: break;
case 14:
- std::strncpy(info->ip_address, QueryHostNetworkInfo().ip_address,
+ std::strncpy(info->ip_address, host.ip_address,
sizeof(info->ip_address) - 1);
break;
case 15:
- std::strncpy(info->netmask, QueryHostNetworkInfo().netmask, sizeof(info->netmask) - 1);
+ std::strncpy(info->netmask, host.netmask, sizeof(info->netmask) - 1);
break;
case 16:
- std::strncpy(info->default_route, QueryHostNetworkInfo().default_route,
+ std::strncpy(info->default_route, host.default_route,
sizeof(info->default_route) - 1);
break;
case 17:
- std::strncpy(info->primary_dns, QueryHostNetworkInfo().primary_dns,
- sizeof(info->primary_dns) - 1);
+ std::strncpy(info->primary_dns, host.primary_dns, sizeof(info->primary_dns) - 1);
break;
case 18:
- std::strncpy(info->secondary_dns, QueryHostNetworkInfo().secondary_dns,
- sizeof(info->secondary_dns) - 1);
+ std::strncpy(info->secondary_dns, host.secondary_dns, sizeof(info->secondary_dns) - 1);
break;
case 19: info->http_proxy_config = NET_CTL_HTTP_PROXY_OFF; break;
case 20: break;
case 21: info->http_proxy_port = 0; break;
- default: LOGF("\t unknown NetCtl info code: %d\n", code); return NET_CTL_ERROR_INVALID_CODE;
}
return OK;
diff --git a/src/main.cpp b/src/main.cpp
index 8057e56f1..a96101022 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -70,6 +70,7 @@ static void PrintUsage() {
::printf(" --vblank-frequency Virtual vblank frequency. Default: 60.\n");
::printf(" --console-language <0-29> Console language. Default: 1 (English US).\n");
::printf(" --vulkan-validation Enable Vulkan validation.\n");
+ ::printf(" --network-connected Use the host local network (default: false).\n");
::printf(" --gpu-assisted-validation Bounds-check shader accesses on the GPU.\n"
" Implies --vulkan-validation; very slow.\n");
::printf(" --shader-validation Enable shader validation.\n");
@@ -388,6 +389,11 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
::printf("invalid console language: %s\n", value.c_str());
return false;
}
+ } else if (arg == "--network-connected") {
+ if (!ParseBool(value, options.config.network_connected)) {
+ ::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
+ return false;
+ }
} else if (arg == "--vulkan-validation") {
if (!ParseBool(value, options.config.vulkan_validation_enabled)) {
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
diff --git a/tests/KernelFileSystemTests.cpp b/tests/KernelFileSystemTests.cpp
index a631dfeb8..1da819da6 100644
--- a/tests/KernelFileSystemTests.cpp
+++ b/tests/KernelFileSystemTests.cpp
@@ -35,6 +35,9 @@
#include
#if defined(__linux__)
+#include
+#include
+#include
#include
#include
#endif
@@ -1459,6 +1462,84 @@ void CheckSocketReceiveBuffer(int reader, int writer) {
}
#endif
+#if defined(__linux__)
+void KYTY_SYSV_ABI RecordNetCtlEvent(int event, void* arg) {
+ auto& events = *static_cast*>(arg);
+ ++events[0];
+ events[1] = event;
+}
+
+void TestNetCtlHostIpv4() {
+ using namespace Libs::Network::NetCtl;
+ ifaddrs* interfaces = nullptr;
+ Check(getifaddrs(&interfaces) == 0, "enumerate host IPv4 addresses");
+ std::vector host_addresses;
+ for (auto* interface = interfaces; interface != nullptr; interface = interface->ifa_next) {
+ if (interface->ifa_addr == nullptr || interface->ifa_addr->sa_family != AF_INET ||
+ (interface->ifa_flags & IFF_UP) == 0 || (interface->ifa_flags & IFF_RUNNING) == 0 ||
+ (interface->ifa_flags & IFF_LOOPBACK) != 0) {
+ continue;
+ }
+ const auto address = reinterpret_cast(interface->ifa_addr)->sin_addr;
+ if (address.s_addr != 0 && (ntohl(address.s_addr) >> 24) != 127) {
+ host_addresses.push_back(address.s_addr);
+ }
+ }
+ freeifaddrs(interfaces);
+
+ Config::ConfigOptions options;
+ options.printf_direction = Config::LogDirection::Silent;
+ Check(!options.network_connected, "host networking requires explicit opt-in");
+ alignas(uint32_t) std::array storage {};
+ auto* info = reinterpret_cast(storage.data());
+ constexpr int NotConnected = static_cast(0x80412108u);
+ Check(NetCtlGetInfo(14, nullptr) == static_cast(0x80412107u) &&
+ NetCtlGetInfo(0, info) == static_cast(0x80412106u) &&
+ NetCtlGetInfo(22, info) == static_cast(0x80412106u),
+ "NetCtl reports invalid output addresses and information codes");
+ for (bool enabled : {false, true}) {
+ options.network_connected = enabled;
+ Config::Load(options);
+ Check(NetCtlInit() == OK, "initialize NetCtl for selected network mode");
+ const bool connected = enabled && !host_addresses.empty();
+ int state = -1;
+ Check(NetCtlGetState(&state) == OK && state == (connected ? 3 : 0),
+ "NetCtl IPv4 state matches opt-in and host connectivity");
+ Check(NetCtlGetStateV6(&state) == OK && state == 0,
+ "IPv4 connectivity does not advertise IPv6 support");
+
+ Check(NetCtlGetInfo(14, info) == (connected ? OK : NotConnected),
+ "NetCtl IPv4 query respects network mode and host state");
+ in_addr address {};
+ if (connected) {
+ Check(inet_pton(AF_INET, storage.data(), &address) == 1 &&
+ std::find(host_addresses.begin(), host_addresses.end(), address.s_addr) !=
+ host_addresses.end(),
+ "NetCtl reports an IPv4 address assigned to an active host interface");
+ }
+ Check(NetCtlGetInfo(15, info) == (connected ? OK : NotConnected) &&
+ (!connected || inet_pton(AF_INET, storage.data(), &address) == 1),
+ "NetCtl returns the active IPv4 netmask only in connected mode");
+ const int gateway_result = NetCtlGetInfo(16, info);
+ Check(gateway_result == NotConnected ||
+ (connected && gateway_result == OK &&
+ inet_pton(AF_INET, storage.data(), &address) == 1),
+ "NetCtl reports a valid IPv4 gateway or its absence");
+
+ std::array events {};
+ int callback_id = -1;
+ Check(NetCtlRegisterCallback(RecordNetCtlEvent, &events, &callback_id) == OK &&
+ NetCtlCheckCallback() == OK && events[0] == 1 &&
+ events[1] == (connected ? 3 : 1) && NetCtlCheckCallback() == OK && events[0] == 1,
+ "NetCtl delivers the matching connection event once while state is unchanged");
+ Check(NetCtlUnregisterCallback(callback_id) == OK, "release NetCtl callback");
+ NetCtlTerm();
+ }
+ options.network_connected = false;
+ Config::Load(options);
+}
+#endif
+
void CheckEtherAddressFormatting() {
Loader::SymbolDatabase symbols;
Libs::LibNet::InitNet_1_Net(&symbols);
@@ -1790,6 +1871,9 @@ int main(int, char**) {
FileSystem::Shutdown();
CheckSocketWakeup();
CheckEtherAddressFormatting();
+#if defined(__linux__)
+ TestNetCtlHostIpv4();
+#endif
TestNpWebApi2Memory();
TestNpCommerceDialog();
graphics.reset();