Merge pull request 'fix: Multiplayer network fixes and airplane mode' (#35) from fix/multiplayer-network-improvements into main

Reviewed-on: https://git.citron-emu.org/Citron/Emulator/pulls/35
This commit is contained in:
Zephyron
2025-11-10 07:19:29 +00:00
16 changed files with 129 additions and 15 deletions

View File

@@ -31,7 +31,8 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
SHOW_RAM_METER("show_ram_meter"), SHOW_RAM_METER("show_ram_meter"),
SHOW_SHADER_BUILDING_OVERLAY("show_shader_building_overlay"), SHOW_SHADER_BUILDING_OVERLAY("show_shader_building_overlay"),
SHOW_PERFORMANCE_GRAPH("show_performance_graph"), SHOW_PERFORMANCE_GRAPH("show_performance_graph"),
USE_CONDITIONAL_RENDERING("use_conditional_rendering"); USE_CONDITIONAL_RENDERING("use_conditional_rendering"),
AIRPLANE_MODE("airplane_mode");
override fun getBoolean(needsGlobal: Boolean): Boolean = override fun getBoolean(needsGlobal: Boolean): Boolean =
NativeConfig.getBoolean(key, needsGlobal) NativeConfig.getBoolean(key, needsGlobal)

View File

@@ -24,6 +24,7 @@ object Settings {
SECTION_INPUT_PLAYER_EIGHT, SECTION_INPUT_PLAYER_EIGHT,
SECTION_THEME(R.string.preferences_theme), SECTION_THEME(R.string.preferences_theme),
SECTION_DEBUG(R.string.preferences_debug), SECTION_DEBUG(R.string.preferences_debug),
SECTION_NETWORK(R.string.preferences_network),
SECTION_ZEP_ZONE(R.string.preferences_zep_zone), SECTION_ZEP_ZONE(R.string.preferences_zep_zone),
SECTION_APPLETS_ANDROID(R.string.preferences_applets_android); SECTION_APPLETS_ANDROID(R.string.preferences_applets_android);
} }

View File

@@ -99,6 +99,7 @@ class SettingsFragmentPresenter(
MenuTag.SECTION_INPUT_PLAYER_EIGHT -> addInputPlayer(sl, 7) MenuTag.SECTION_INPUT_PLAYER_EIGHT -> addInputPlayer(sl, 7)
MenuTag.SECTION_THEME -> addThemeSettings(sl) MenuTag.SECTION_THEME -> addThemeSettings(sl)
MenuTag.SECTION_DEBUG -> addDebugSettings(sl) MenuTag.SECTION_DEBUG -> addDebugSettings(sl)
MenuTag.SECTION_NETWORK -> addNetworkSettings(sl)
MenuTag.SECTION_ZEP_ZONE -> addZepZoneSettings(sl) MenuTag.SECTION_ZEP_ZONE -> addZepZoneSettings(sl)
MenuTag.SECTION_APPLETS_ANDROID -> addAppletsAndroidSettings(sl) MenuTag.SECTION_APPLETS_ANDROID -> addAppletsAndroidSettings(sl)
} }
@@ -144,6 +145,14 @@ class SettingsFragmentPresenter(
menuKey = MenuTag.SECTION_DEBUG menuKey = MenuTag.SECTION_DEBUG
) )
) )
add(
SubmenuSetting(
titleId = R.string.preferences_network,
descriptionId = R.string.preferences_network_description,
iconId = R.drawable.ic_settings,
menuKey = MenuTag.SECTION_NETWORK
)
)
add( add(
SubmenuSetting( SubmenuSetting(
titleId = R.string.preferences_zep_zone, titleId = R.string.preferences_zep_zone,
@@ -1002,6 +1011,13 @@ class SettingsFragmentPresenter(
} }
} }
private fun addNetworkSettings(sl: ArrayList<SettingsItem>) {
sl.apply {
add(HeaderSetting(R.string.network_settings_header))
add(BooleanSetting.AIRPLANE_MODE.key)
}
}
private fun addZepZoneSettings(sl: ArrayList<SettingsItem>) { private fun addZepZoneSettings(sl: ArrayList<SettingsItem>) {
sl.apply { sl.apply {
add(HeaderSetting(R.string.memory_layout_header)) add(HeaderSetting(R.string.memory_layout_header))

View File

@@ -421,11 +421,16 @@
<string name="preferences_theme">Theme and color</string> <string name="preferences_theme">Theme and color</string>
<string name="preferences_debug">Debug</string> <string name="preferences_debug">Debug</string>
<string name="preferences_debug_description">CPU/GPU debugging, graphics API, fastmem</string> <string name="preferences_debug_description">CPU/GPU debugging, graphics API, fastmem</string>
<string name="preferences_network">Network</string>
<string name="preferences_network_description">Network interface and airplane mode settings</string>
<string name="preferences_zep_zone">Zep Zone</string> <string name="preferences_zep_zone">Zep Zone</string>
<string name="preferences_zep_zone_description">Advanced emulation settings</string> <string name="preferences_zep_zone_description">Advanced emulation settings</string>
<string name="preferences_applets_android">Applets on Android</string> <string name="preferences_applets_android">Applets on Android</string>
<string name="preferences_applets_android_description">System applet configuration settings</string> <string name="preferences_applets_android_description">System applet configuration settings</string>
<!-- Network Settings Headers -->
<string name="network_settings_header">Network Settings</string>
<!-- Zep Zone Headers --> <!-- Zep Zone Headers -->
<string name="memory_layout_header">Memory Layout</string> <string name="memory_layout_header">Memory Layout</string>
<string name="astc_settings_header">ASTC Settings</string> <string name="astc_settings_header">ASTC Settings</string>

View File

@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include <QtConcurrent/QtConcurrent> #include <QtConcurrent/QtConcurrent>
@@ -23,6 +24,7 @@ ConfigureNetwork::ConfigureNetwork(const Core::System& system_, QWidget* parent)
ConfigureNetwork::~ConfigureNetwork() = default; ConfigureNetwork::~ConfigureNetwork() = default;
void ConfigureNetwork::ApplyConfiguration() { void ConfigureNetwork::ApplyConfiguration() {
Settings::values.airplane_mode = ui->airplane_mode->isChecked();
Settings::values.network_interface = ui->network_interface->currentText().toStdString(); Settings::values.network_interface = ui->network_interface->currentText().toStdString();
} }
@@ -41,8 +43,15 @@ void ConfigureNetwork::RetranslateUI() {
void ConfigureNetwork::SetConfiguration() { void ConfigureNetwork::SetConfiguration() {
const bool runtime_lock = !system.IsPoweredOn(); const bool runtime_lock = !system.IsPoweredOn();
ui->airplane_mode->setChecked(Settings::values.airplane_mode.GetValue());
ui->airplane_mode->setEnabled(runtime_lock);
const std::string& network_interface = Settings::values.network_interface.GetValue(); const std::string& network_interface = Settings::values.network_interface.GetValue();
ui->network_interface->setCurrentText(QString::fromStdString(network_interface)); ui->network_interface->setCurrentText(QString::fromStdString(network_interface));
ui->network_interface->setEnabled(runtime_lock); ui->network_interface->setEnabled(runtime_lock && !ui->airplane_mode->isChecked());
connect(ui->airplane_mode, &QCheckBox::toggled, this, [this](bool checked) {
ui->network_interface->setEnabled(!checked && !system.IsPoweredOn());
});
} }

View File

@@ -25,6 +25,16 @@
<string>General</string> <string>General</string>
</property> </property>
<layout class="QGridLayout" name="gridLayout_2"> <layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QCheckBox" name="airplane_mode">
<property name="text">
<string>Airplane Mode</string>
</property>
<property name="toolTip">
<string>Disable all network functionality, similar to Nintendo Switch airplane mode</string>
</property>
</widget>
</item>
<item row="1" column="1"> <item row="1" column="1">
<widget class="QComboBox" name="network_interface"/> <widget class="QComboBox" name="network_interface"/>
</item> </item>

View File

@@ -7,6 +7,7 @@
#include <QRegularExpressionValidator> #include <QRegularExpressionValidator>
#include <QString> #include <QString>
#include <QtConcurrent/QtConcurrentRun> #include <QtConcurrent/QtConcurrentRun>
#include "common/logging/log.h"
#include "common/settings.h" #include "common/settings.h"
#include "core/core.h" #include "core/core.h"
#include "core/internal_network/network_interface.h" #include "core/internal_network/network_interface.h"
@@ -57,6 +58,11 @@ void DirectConnectWindow::RetranslateUi() {
} }
void DirectConnectWindow::Connect() { void DirectConnectWindow::Connect() {
if (!Network::GetSelectedNetworkInterface()) {
LOG_INFO(WebService, "Automatically selected network interface for room network.");
Network::SelectFirstNetworkInterface();
}
if (!Network::GetSelectedNetworkInterface()) { if (!Network::GetSelectedNetworkInterface()) {
NetworkMessage::ErrorManager::ShowError( NetworkMessage::ErrorManager::ShowError(
NetworkMessage::ErrorManager::NO_INTERFACE_SELECTED); NetworkMessage::ErrorManager::NO_INTERFACE_SELECTED);

View File

@@ -112,6 +112,11 @@ std::unique_ptr<Network::VerifyUser::Backend> HostRoomWindow::CreateVerifyBacken
} }
void HostRoomWindow::Host() { void HostRoomWindow::Host() {
if (!Network::GetSelectedNetworkInterface()) {
LOG_INFO(WebService, "Automatically selected network interface for room network.");
Network::SelectFirstNetworkInterface();
}
if (!Network::GetSelectedNetworkInterface()) { if (!Network::GetSelectedNetworkInterface()) {
NetworkMessage::ErrorManager::ShowError( NetworkMessage::ErrorManager::ShowError(
NetworkMessage::ErrorManager::NO_INTERFACE_SELECTED); NetworkMessage::ErrorManager::NO_INTERFACE_SELECTED);
@@ -208,13 +213,18 @@ void HostRoomWindow::Host() {
Settings::values.citron_username.GetValue(), Settings::values.citron_username.GetValue(),
Settings::values.citron_token.GetValue()); Settings::values.citron_token.GetValue());
if (auto room = room_network.GetRoom().lock()) { if (auto room = room_network.GetRoom().lock()) {
token = client.GetExternalJWT(room->GetVerifyUID()).returned_data; const std::string verify_uid = room->GetVerifyUID();
} if (!verify_uid.empty()) {
token = client.GetExternalJWT(verify_uid).returned_data;
if (token.empty()) { if (token.empty()) {
LOG_ERROR(WebService, "Could not get external JWT, verification may fail"); LOG_ERROR(WebService, "Could not get external JWT, verification may fail");
} else { } else {
LOG_INFO(WebService, "Successfully requested external JWT: size={}", token.size()); LOG_INFO(WebService, "Successfully requested external JWT: size={}", token.size());
} }
} else {
LOG_DEBUG(WebService, "Skipping JWT request: verify_uid is empty (room may not require verification)");
}
}
} }
#endif #endif
// TODO: Check what to do with this // TODO: Check what to do with this

View File

@@ -188,7 +188,7 @@ void Lobby::OnJoinRoom(const QModelIndex& source) {
std::string token; std::string token;
#ifdef ENABLE_WEB_SERVICE #ifdef ENABLE_WEB_SERVICE
if (!Settings::values.citron_username.GetValue().empty() && if (!Settings::values.citron_username.GetValue().empty() &&
!Settings::values.citron_token.GetValue().empty()) { !Settings::values.citron_token.GetValue().empty() && !verify_uid.empty()) {
WebService::Client client(Settings::values.web_api_url.GetValue(), WebService::Client client(Settings::values.web_api_url.GetValue(),
Settings::values.citron_username.GetValue(), Settings::values.citron_username.GetValue(),
Settings::values.citron_token.GetValue()); Settings::values.citron_token.GetValue());
@@ -198,6 +198,8 @@ void Lobby::OnJoinRoom(const QModelIndex& source) {
} else { } else {
LOG_INFO(WebService, "Successfully requested external JWT: size={}", token.size()); LOG_INFO(WebService, "Successfully requested external JWT: size={}", token.size());
} }
} else if (verify_uid.empty()) {
LOG_DEBUG(WebService, "Skipping JWT request: verify_uid is empty (room may not require verification)");
} }
#endif #endif
if (auto room_member = room_network.GetRoomMember().lock()) { if (auto room_member = room_network.GetRoomMember().lock()) {

View File

@@ -660,6 +660,7 @@ struct Values {
Setting<bool> use_dev_keys{linkage, false, "use_dev_keys", Category::Miscellaneous}; Setting<bool> use_dev_keys{linkage, false, "use_dev_keys", Category::Miscellaneous};
// Network // Network
Setting<bool> airplane_mode{linkage, false, "airplane_mode", Category::Network};
Setting<std::string> network_interface{linkage, std::string(), "network_interface", Setting<std::string> network_interface{linkage, std::string(), "network_interface",
Category::Network}; Category::Network};
Setting<std::string> lobby_api_url{linkage, "https://api.ynet-fun.xyz", "lobby_api_url", Setting<std::string> lobby_api_url{linkage, "https://api.ynet-fun.xyz", "lobby_api_url",

View File

@@ -52,6 +52,7 @@ enum class ErrorModule : u32 {
Util = 33, Util = 33,
TIPC = 35, TIPC = 35,
ANIF = 37, ANIF = 37,
Module38 = 38, // Unknown/Undefined module - stubbed for multiplayer compatibility
CRT = 39, CRT = 39,
ETHC = 100, ETHC = 100,
I2C = 101, I2C = 101,

View File

@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include <array> #include <array>
@@ -65,16 +66,30 @@ enum class FatalType : u32 {
static void GenerateErrorReport(Core::System& system, Result error_code, const FatalInfo& info) { static void GenerateErrorReport(Core::System& system, Result error_code, const FatalInfo& info) {
const auto title_id = system.GetApplicationProcessProgramID(); const auto title_id = system.GetApplicationProcessProgramID();
const auto module = static_cast<u32>(error_code.GetModule());
const auto description = static_cast<u32>(error_code.GetDescription());
// Check if this is module 38 (undefined/unknown module)
std::string module_note;
if (module == 38) {
module_note = fmt::format(
"\n⚠️ WARNING: Error module 38 is undefined/unknown!\n"
"This error may be game-generated or from an unimplemented service.\n"
"Error code: 2038-{:04d} (0x{:08X})\n"
"If you're experiencing multiplayer issues, this may be a stubbing issue.\n\n",
description, error_code.raw);
}
std::string crash_report = fmt::format( std::string crash_report = fmt::format(
"Citron {}-{} crash report\n" "Citron {}-{} crash report\n"
"Title ID: {:016x}\n" "Title ID: {:016x}\n"
"Result: 0x{:X} ({:04}-{:04d})\n" "Result: 0x{:X} ({:04}-{:04d})\n"
"Set flags: 0x{:16X}\n" "Set flags: 0x{:16X}\n"
"Program entry point: 0x{:16X}\n" "Program entry point: 0x{:16X}\n"
"{}"
"\n", "\n",
Common::g_scm_branch, Common::g_scm_desc, title_id, error_code.raw, Common::g_scm_branch, Common::g_scm_desc, title_id, error_code.raw,
2000 + static_cast<u32>(error_code.GetModule()), 2000 + module, description, info.set_flags, info.program_entry_point, module_note);
static_cast<u32>(error_code.GetDescription()), info.set_flags, info.program_entry_point);
if (info.backtrace_size != 0x0) { if (info.backtrace_size != 0x0) {
crash_report += "Registers:\n"; crash_report += "Registers:\n";
for (size_t i = 0; i < info.registers.size(); i++) { for (size_t i = 0; i < info.registers.size(); i++) {

View File

@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
#pragma once #pragma once
@@ -24,4 +25,13 @@ constexpr Result ResultLocalCommunicationIdNotFound{ErrorModule::LDN, 97};
constexpr Result ResultLocalCommunicationVersionTooLow{ErrorModule::LDN, 113}; constexpr Result ResultLocalCommunicationVersionTooLow{ErrorModule::LDN, 113};
constexpr Result ResultLocalCommunicationVersionTooHigh{ErrorModule::LDN, 114}; constexpr Result ResultLocalCommunicationVersionTooHigh{ErrorModule::LDN, 114};
// Module 38 error codes - Unknown/undefined module
// These are stubbed to prevent crashes during multiplayer
// Error code format: 2038-XXXX where XXXX is the description
constexpr Result ResultModule38Error2618{ErrorModule::Module38, 2618}; // Reported during multiplayer
constexpr Result ResultModule38Generic{ErrorModule::Module38, 0}; // Generic module 38 error
constexpr Result ResultModule38NetworkError{ErrorModule::Module38, 100}; // Network-related
constexpr Result ResultModule38ConnectionFailed{ErrorModule::Module38, 200}; // Connection failure
constexpr Result ResultModule38Timeout{ErrorModule::Module38, 300}; // Operation timeout
} // namespace Service::LDN } // namespace Service::LDN

View File

@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm> #include <algorithm>
@@ -187,6 +188,11 @@ std::vector<NetworkInterface> GetAvailableNetworkInterfaces() {
#endif #endif
std::optional<NetworkInterface> GetSelectedNetworkInterface() { std::optional<NetworkInterface> GetSelectedNetworkInterface() {
// If airplane mode is enabled, return no interface (similar to Switch's airplane mode)
if (Settings::values.airplane_mode.GetValue()) {
return std::nullopt;
}
const auto& selected_network_interface = Settings::values.network_interface.GetValue(); const auto& selected_network_interface = Settings::values.network_interface.GetValue();
const auto network_interfaces = Network::GetAvailableNetworkInterfaces(); const auto network_interfaces = Network::GetAvailableNetworkInterfaces();
if (network_interfaces.empty()) { if (network_interfaces.empty()) {

View File

@@ -9,6 +9,7 @@
#include <thread> #include <thread>
#include "common/assert.h" #include "common/assert.h"
#include "common/socket_types.h" #include "common/socket_types.h"
#include "core/internal_network/network_interface.h"
#include "enet/enet.h" #include "enet/enet.h"
#include "network/packet.h" #include "network/packet.h"
#include "network/room_member.h" #include "network/room_member.h"
@@ -601,11 +602,23 @@ void RoomMember::Join(const std::string& nick, const char* server_addr, u16 serv
room_member_impl->loop_thread.reset(); room_member_impl->loop_thread.reset();
} }
if (!room_member_impl->client) { // Always recreate the client to ensure it uses the current network interface settings
room_member_impl->client = enet_host_create(nullptr, 1, NumChannels, 0, 0); // This is necessary because the client might have been created with different settings
ASSERT_MSG(room_member_impl->client != nullptr, "Could not create client"); if (room_member_impl->client) {
enet_host_destroy(room_member_impl->client);
room_member_impl->client = nullptr;
} }
// For client connections, bind to ENET_HOST_ANY (0.0.0.0) to allow the OS to route
// based on the destination address. The selected network interface will be used
// by the OS routing table when connecting to the server.
ENetAddress bind_address{};
bind_address.host = ENET_HOST_ANY;
bind_address.port = 0; // Let the system choose an available port
room_member_impl->client = enet_host_create(&bind_address, 1, NumChannels, 0, 0);
ASSERT_MSG(room_member_impl->client != nullptr, "Could not create client");
room_member_impl->SetState(State::Joining); room_member_impl->SetState(State::Joining);
ENetAddress address{}; ENetAddress address{};

View File

@@ -138,7 +138,15 @@ struct Client::Impl {
return WebResult{WebResult::Code::WrongContent, "", ""}; return WebResult{WebResult::Code::WrongContent, "", ""};
} }
if (content_type->second.find(accept) == std::string::npos) { // For GetExternalJWT, be more lenient with content-type to handle error responses
// The API may return text/plain for errors, but we still want to process the response
bool content_type_valid = content_type->second.find(accept) != std::string::npos;
if (!content_type_valid && accept == "text/html") {
// Also accept text/plain for JWT endpoints (error responses)
content_type_valid = content_type->second.find("text/plain") != std::string::npos;
}
if (!content_type_valid) {
LOG_ERROR(WebService, "{} to {} returned wrong content: {}", method, host + path, LOG_ERROR(WebService, "{} to {} returned wrong content: {}", method, host + path,
content_type->second); content_type->second);
return WebResult{WebResult::Code::WrongContent, "Wrong content", ""}; return WebResult{WebResult::Code::WrongContent, "Wrong content", ""};