mirror of
https://git.citron-emu.org/citron/emulator
synced 2025-12-19 10:43:33 +00:00
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:
@@ -31,7 +31,8 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
|
||||
SHOW_RAM_METER("show_ram_meter"),
|
||||
SHOW_SHADER_BUILDING_OVERLAY("show_shader_building_overlay"),
|
||||
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 =
|
||||
NativeConfig.getBoolean(key, needsGlobal)
|
||||
|
||||
@@ -24,6 +24,7 @@ object Settings {
|
||||
SECTION_INPUT_PLAYER_EIGHT,
|
||||
SECTION_THEME(R.string.preferences_theme),
|
||||
SECTION_DEBUG(R.string.preferences_debug),
|
||||
SECTION_NETWORK(R.string.preferences_network),
|
||||
SECTION_ZEP_ZONE(R.string.preferences_zep_zone),
|
||||
SECTION_APPLETS_ANDROID(R.string.preferences_applets_android);
|
||||
}
|
||||
|
||||
@@ -99,6 +99,7 @@ class SettingsFragmentPresenter(
|
||||
MenuTag.SECTION_INPUT_PLAYER_EIGHT -> addInputPlayer(sl, 7)
|
||||
MenuTag.SECTION_THEME -> addThemeSettings(sl)
|
||||
MenuTag.SECTION_DEBUG -> addDebugSettings(sl)
|
||||
MenuTag.SECTION_NETWORK -> addNetworkSettings(sl)
|
||||
MenuTag.SECTION_ZEP_ZONE -> addZepZoneSettings(sl)
|
||||
MenuTag.SECTION_APPLETS_ANDROID -> addAppletsAndroidSettings(sl)
|
||||
}
|
||||
@@ -144,6 +145,14 @@ class SettingsFragmentPresenter(
|
||||
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(
|
||||
SubmenuSetting(
|
||||
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>) {
|
||||
sl.apply {
|
||||
add(HeaderSetting(R.string.memory_layout_header))
|
||||
|
||||
@@ -421,11 +421,16 @@
|
||||
<string name="preferences_theme">Theme and color</string>
|
||||
<string name="preferences_debug">Debug</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_description">Advanced emulation settings</string>
|
||||
<string name="preferences_applets_android">Applets on Android</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 -->
|
||||
<string name="memory_layout_header">Memory Layout</string>
|
||||
<string name="astc_settings_header">ASTC Settings</string>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <QtConcurrent/QtConcurrent>
|
||||
@@ -23,6 +24,7 @@ ConfigureNetwork::ConfigureNetwork(const Core::System& system_, QWidget* parent)
|
||||
ConfigureNetwork::~ConfigureNetwork() = default;
|
||||
|
||||
void ConfigureNetwork::ApplyConfiguration() {
|
||||
Settings::values.airplane_mode = ui->airplane_mode->isChecked();
|
||||
Settings::values.network_interface = ui->network_interface->currentText().toStdString();
|
||||
}
|
||||
|
||||
@@ -41,8 +43,15 @@ void ConfigureNetwork::RetranslateUI() {
|
||||
void ConfigureNetwork::SetConfiguration() {
|
||||
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();
|
||||
|
||||
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());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -25,6 +25,16 @@
|
||||
<string>General</string>
|
||||
</property>
|
||||
<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">
|
||||
<widget class="QComboBox" name="network_interface"/>
|
||||
</item>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <QRegularExpressionValidator>
|
||||
#include <QString>
|
||||
#include <QtConcurrent/QtConcurrentRun>
|
||||
#include "common/logging/log.h"
|
||||
#include "common/settings.h"
|
||||
#include "core/core.h"
|
||||
#include "core/internal_network/network_interface.h"
|
||||
@@ -57,6 +58,11 @@ void DirectConnectWindow::RetranslateUi() {
|
||||
}
|
||||
|
||||
void DirectConnectWindow::Connect() {
|
||||
if (!Network::GetSelectedNetworkInterface()) {
|
||||
LOG_INFO(WebService, "Automatically selected network interface for room network.");
|
||||
Network::SelectFirstNetworkInterface();
|
||||
}
|
||||
|
||||
if (!Network::GetSelectedNetworkInterface()) {
|
||||
NetworkMessage::ErrorManager::ShowError(
|
||||
NetworkMessage::ErrorManager::NO_INTERFACE_SELECTED);
|
||||
|
||||
@@ -112,6 +112,11 @@ std::unique_ptr<Network::VerifyUser::Backend> HostRoomWindow::CreateVerifyBacken
|
||||
}
|
||||
|
||||
void HostRoomWindow::Host() {
|
||||
if (!Network::GetSelectedNetworkInterface()) {
|
||||
LOG_INFO(WebService, "Automatically selected network interface for room network.");
|
||||
Network::SelectFirstNetworkInterface();
|
||||
}
|
||||
|
||||
if (!Network::GetSelectedNetworkInterface()) {
|
||||
NetworkMessage::ErrorManager::ShowError(
|
||||
NetworkMessage::ErrorManager::NO_INTERFACE_SELECTED);
|
||||
@@ -208,12 +213,17 @@ void HostRoomWindow::Host() {
|
||||
Settings::values.citron_username.GetValue(),
|
||||
Settings::values.citron_token.GetValue());
|
||||
if (auto room = room_network.GetRoom().lock()) {
|
||||
token = client.GetExternalJWT(room->GetVerifyUID()).returned_data;
|
||||
}
|
||||
if (token.empty()) {
|
||||
LOG_ERROR(WebService, "Could not get external JWT, verification may fail");
|
||||
} else {
|
||||
LOG_INFO(WebService, "Successfully requested external JWT: size={}", token.size());
|
||||
const std::string verify_uid = room->GetVerifyUID();
|
||||
if (!verify_uid.empty()) {
|
||||
token = client.GetExternalJWT(verify_uid).returned_data;
|
||||
if (token.empty()) {
|
||||
LOG_ERROR(WebService, "Could not get external JWT, verification may fail");
|
||||
} else {
|
||||
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
|
||||
|
||||
@@ -188,7 +188,7 @@ void Lobby::OnJoinRoom(const QModelIndex& source) {
|
||||
std::string token;
|
||||
#ifdef ENABLE_WEB_SERVICE
|
||||
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(),
|
||||
Settings::values.citron_username.GetValue(),
|
||||
Settings::values.citron_token.GetValue());
|
||||
@@ -198,6 +198,8 @@ void Lobby::OnJoinRoom(const QModelIndex& source) {
|
||||
} else {
|
||||
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
|
||||
if (auto room_member = room_network.GetRoomMember().lock()) {
|
||||
|
||||
@@ -660,6 +660,7 @@ struct Values {
|
||||
Setting<bool> use_dev_keys{linkage, false, "use_dev_keys", Category::Miscellaneous};
|
||||
|
||||
// Network
|
||||
Setting<bool> airplane_mode{linkage, false, "airplane_mode", Category::Network};
|
||||
Setting<std::string> network_interface{linkage, std::string(), "network_interface",
|
||||
Category::Network};
|
||||
Setting<std::string> lobby_api_url{linkage, "https://api.ynet-fun.xyz", "lobby_api_url",
|
||||
|
||||
@@ -52,6 +52,7 @@ enum class ErrorModule : u32 {
|
||||
Util = 33,
|
||||
TIPC = 35,
|
||||
ANIF = 37,
|
||||
Module38 = 38, // Unknown/Undefined module - stubbed for multiplayer compatibility
|
||||
CRT = 39,
|
||||
ETHC = 100,
|
||||
I2C = 101,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <array>
|
||||
@@ -65,16 +66,30 @@ enum class FatalType : u32 {
|
||||
|
||||
static void GenerateErrorReport(Core::System& system, Result error_code, const FatalInfo& info) {
|
||||
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(
|
||||
"Citron {}-{} crash report\n"
|
||||
"Title ID: {:016x}\n"
|
||||
"Result: 0x{:X} ({:04}-{:04d})\n"
|
||||
"Set flags: 0x{:16X}\n"
|
||||
"Program entry point: 0x{:16X}\n"
|
||||
"{}"
|
||||
"\n",
|
||||
Common::g_scm_branch, Common::g_scm_desc, title_id, error_code.raw,
|
||||
2000 + static_cast<u32>(error_code.GetModule()),
|
||||
static_cast<u32>(error_code.GetDescription()), info.set_flags, info.program_entry_point);
|
||||
2000 + module, description, info.set_flags, info.program_entry_point, module_note);
|
||||
if (info.backtrace_size != 0x0) {
|
||||
crash_report += "Registers:\n";
|
||||
for (size_t i = 0; i < info.registers.size(); i++) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
@@ -24,4 +25,13 @@ constexpr Result ResultLocalCommunicationIdNotFound{ErrorModule::LDN, 97};
|
||||
constexpr Result ResultLocalCommunicationVersionTooLow{ErrorModule::LDN, 113};
|
||||
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
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
@@ -187,6 +188,11 @@ std::vector<NetworkInterface> GetAvailableNetworkInterfaces() {
|
||||
#endif
|
||||
|
||||
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 network_interfaces = Network::GetAvailableNetworkInterfaces();
|
||||
if (network_interfaces.empty()) {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <thread>
|
||||
#include "common/assert.h"
|
||||
#include "common/socket_types.h"
|
||||
#include "core/internal_network/network_interface.h"
|
||||
#include "enet/enet.h"
|
||||
#include "network/packet.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();
|
||||
}
|
||||
|
||||
if (!room_member_impl->client) {
|
||||
room_member_impl->client = enet_host_create(nullptr, 1, NumChannels, 0, 0);
|
||||
ASSERT_MSG(room_member_impl->client != nullptr, "Could not create client");
|
||||
// Always recreate the client to ensure it uses the current network interface settings
|
||||
// This is necessary because the client might have been created with different settings
|
||||
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);
|
||||
|
||||
ENetAddress address{};
|
||||
|
||||
@@ -138,7 +138,15 @@ struct Client::Impl {
|
||||
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,
|
||||
content_type->second);
|
||||
return WebResult{WebResult::Code::WrongContent, "Wrong content", ""};
|
||||
|
||||
Reference in New Issue
Block a user