netctl: add opt-in host IPv4 connectivity and launcher toggle

This commit is contained in:
nmzik
2026-10-12 01:19:07 +02:00
parent 41beda4367
commit e699a35f20
10 changed files with 206 additions and 57 deletions
+4
View File
@@ -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;
}
+2
View File
@@ -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();
@@ -329,6 +329,23 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="network_tab">
<attribute name="title"><string>Network</string></attribute>
<layout class="QVBoxLayout" name="networkTabLayout">
<item>
<widget class="QCheckBox" name="checkBox_network_connected">
<property name="toolTip"><string>Let games use your computer's local network connection.</string></property>
<property name="text"><string>Enable network connection</string></property>
</widget>
</item>
<item>
<spacer name="networkSpacer">
<property name="orientation"><enum>Qt::Vertical</enum></property>
<property name="sizeHint" stdset="0"><size><width>20</width><height>40</height></size></property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="debug_tab">
<attribute name="title"><string>Debug</string></attribute>
<layout class="QVBoxLayout" name="debugTabLayout">
+2
View File
@@ -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;
+2
View File
@@ -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) {
@@ -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();
+1
View File
@@ -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";
}
+86 -57
View File
@@ -15,6 +15,9 @@
#else
#include <arpa/inet.h>
#include <fcntl.h>
#include <ifaddrs.h>
#include <net/if.h>
#include <net/route.h>
#include <netdb.h>
#include <netinet/tcp.h>
#include <sys/select.h>
@@ -41,10 +44,12 @@
#include <cstring>
#include <fmt/format.h>
#include <fmt/ranges.h>
#include <fstream>
#include <limits>
#include <memory>
#include <mutex>
#include <optional>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
@@ -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<NetCtlCallbackSlot, 8> 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<const sockaddr_in*>(addr);
inet_ntop(AF_INET, &in->sin_addr, dst, static_cast<socklen_t>(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<socklen_t>(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<uint8_t> buffer(size);
auto* adapters = reinterpret_cast<IP_ADAPTER_ADDRESSES*>(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<const sockaddr_in6*>(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<const sockaddr_in*>(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<uint32_t>::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;
+6
View File
@@ -70,6 +70,7 @@ static void PrintUsage() {
::printf(" --vblank-frequency <num> Virtual vblank frequency. Default: 60.\n");
::printf(" --console-language <0-29> Console language. Default: 1 (English US).\n");
::printf(" --vulkan-validation <true|false> Enable Vulkan validation.\n");
::printf(" --network-connected <true|false> Use the host local network (default: false).\n");
::printf(" --gpu-assisted-validation <t|f> Bounds-check shader accesses on the GPU.\n"
" Implies --vulkan-validation; very slow.\n");
::printf(" --shader-validation <true|false> 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());