1285 lines
44 KiB
C++
1285 lines
44 KiB
C++
// Copyright 2017 Dolphin Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
#include "DolphinQt/NetPlay/NetPlayDialog.h"
|
|
|
|
#include <QAction>
|
|
#include <QActionGroup>
|
|
#include <QApplication>
|
|
#include <QClipboard>
|
|
#include <QComboBox>
|
|
#include <QFileDialog>
|
|
#include <QGridLayout>
|
|
#include <QGroupBox>
|
|
#include <QHeaderView>
|
|
#include <QLabel>
|
|
#include <QLineEdit>
|
|
#include <QMenu>
|
|
#include <QPushButton>
|
|
#include <QSignalBlocker>
|
|
#include <QSpinBox>
|
|
#include <QSplitter>
|
|
#include <QTableWidget>
|
|
#include <QTextBrowser>
|
|
|
|
#include <algorithm>
|
|
#include <sstream>
|
|
|
|
#include <fmt/ranges.h>
|
|
|
|
#include "Common/CommonPaths.h"
|
|
#include "Common/Config/Config.h"
|
|
#include "Common/HttpRequest.h"
|
|
#include "Common/Logging/Log.h"
|
|
#include "Common/TraversalClient.h"
|
|
|
|
#include "Core/Boot/Boot.h"
|
|
#include "Core/Config/GraphicsSettings.h"
|
|
#include "Core/Config/MainSettings.h"
|
|
#include "Core/Config/NetplaySettings.h"
|
|
#include "Core/ConfigManager.h"
|
|
#include "Core/Core.h"
|
|
#ifdef HAS_LIBMGBA
|
|
#include "Core/HW/GBACore.h"
|
|
#endif
|
|
#include "Core/IOS/FS/FileSystem.h"
|
|
#include "Core/NetPlayServer.h"
|
|
#include "Core/SyncIdentifier.h"
|
|
#include "Core/System.h"
|
|
|
|
#include "DolphinQt/NetPlay/ChunkedProgressDialog.h"
|
|
#include "DolphinQt/NetPlay/GameDigestDialog.h"
|
|
#include "DolphinQt/NetPlay/GameListDialog.h"
|
|
#include "DolphinQt/NetPlay/PadMappingDialog.h"
|
|
#include "DolphinQt/QtUtils/ModalMessageBox.h"
|
|
#include "DolphinQt/QtUtils/QueueOnObject.h"
|
|
#include "DolphinQt/QtUtils/RunOnObject.h"
|
|
#include "DolphinQt/QtUtils/SetWindowDecorations.h"
|
|
#include "DolphinQt/Resources.h"
|
|
#include "DolphinQt/Settings.h"
|
|
#include "DolphinQt/Settings/GameCubePane.h"
|
|
|
|
#include "UICommon/DiscordPresence.h"
|
|
#include "UICommon/GameFile.h"
|
|
#include "UICommon/UICommon.h"
|
|
|
|
#include "VideoCommon/NetPlayChatUI.h"
|
|
#include "VideoCommon/NetPlayGolfUI.h"
|
|
#include "VideoCommon/VideoConfig.h"
|
|
|
|
namespace
|
|
{
|
|
QString InetAddressToString(const Common::TraversalInetAddress& addr)
|
|
{
|
|
QString ip;
|
|
|
|
if (addr.isIPV6)
|
|
{
|
|
ip = QStringLiteral("IPv6-Not-Implemented");
|
|
}
|
|
else
|
|
{
|
|
const auto ipv4 = reinterpret_cast<const u8*>(addr.address);
|
|
ip = QString::number(ipv4[0]);
|
|
for (u32 i = 1; i != 4; ++i)
|
|
{
|
|
ip += QStringLiteral(".");
|
|
ip += QString::number(ipv4[i]);
|
|
}
|
|
}
|
|
|
|
return QStringLiteral("%1:%2").arg(ip, QString::number(ntohs(addr.port)));
|
|
}
|
|
} // namespace
|
|
|
|
NetPlayDialog::NetPlayDialog(const GameListModel& game_list_model,
|
|
StartGameCallback start_game_callback, QWidget* parent)
|
|
: QDialog(parent), m_game_list_model(game_list_model),
|
|
m_start_game_callback(std::move(start_game_callback))
|
|
{
|
|
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
|
|
|
|
setWindowTitle(tr("NetPlay"));
|
|
setWindowIcon(Resources::GetAppIcon());
|
|
|
|
m_pad_mapping = new PadMappingDialog(this);
|
|
m_game_digest_dialog = new GameDigestDialog(this);
|
|
m_chunked_progress_dialog = new ChunkedProgressDialog(this);
|
|
|
|
ResetExternalIP();
|
|
CreateChatLayout();
|
|
CreatePlayersLayout();
|
|
CreateMainLayout();
|
|
LoadSettings();
|
|
ConnectWidgets();
|
|
|
|
auto& settings = Settings::Instance().GetQSettings();
|
|
|
|
restoreGeometry(settings.value(QStringLiteral("netplaydialog/geometry")).toByteArray());
|
|
m_splitter->restoreState(settings.value(QStringLiteral("netplaydialog/splitter")).toByteArray());
|
|
}
|
|
|
|
NetPlayDialog::~NetPlayDialog()
|
|
{
|
|
auto& settings = Settings::Instance().GetQSettings();
|
|
|
|
settings.setValue(QStringLiteral("netplaydialog/geometry"), saveGeometry());
|
|
settings.setValue(QStringLiteral("netplaydialog/splitter"), m_splitter->saveState());
|
|
}
|
|
|
|
void NetPlayDialog::CreateMainLayout()
|
|
{
|
|
m_main_layout = new QGridLayout;
|
|
m_game_button = new QPushButton;
|
|
m_start_button = new QPushButton(tr("Start"));
|
|
m_buffer_size_box = new QSpinBox;
|
|
m_buffer_label = new QLabel(tr("Buffer:"));
|
|
m_quit_button = new QPushButton(tr("Quit"));
|
|
m_splitter = new QSplitter(Qt::Horizontal);
|
|
m_menu_bar = new QMenuBar(this);
|
|
|
|
m_data_menu = m_menu_bar->addMenu(tr("Data"));
|
|
m_data_menu->setToolTipsVisible(true);
|
|
|
|
m_savedata_none_action = m_data_menu->addAction(tr("No Save Data"));
|
|
m_savedata_none_action->setToolTip(
|
|
tr("Netplay will start without any save data, and any created save data will be discarded at "
|
|
"the end of the Netplay session."));
|
|
m_savedata_none_action->setCheckable(true);
|
|
m_savedata_load_only_action = m_data_menu->addAction(tr("Load Host's Save Data Only"));
|
|
m_savedata_load_only_action->setToolTip(tr(
|
|
"Netplay will start using the Host's save data, but any save data created or modified during "
|
|
"the Netplay session will be discarded at the end of the session."));
|
|
m_savedata_load_only_action->setCheckable(true);
|
|
m_savedata_load_and_write_action = m_data_menu->addAction(tr("Load and Write Host's Save Data"));
|
|
m_savedata_load_and_write_action->setToolTip(
|
|
tr("Netplay will start using the Host's save data, and any save data created or modified "
|
|
"during the Netplay session will remain in the Host's local saves."));
|
|
m_savedata_load_and_write_action->setCheckable(true);
|
|
|
|
m_savedata_style_group = new QActionGroup(this);
|
|
m_savedata_style_group->setExclusive(true);
|
|
m_savedata_style_group->addAction(m_savedata_none_action);
|
|
m_savedata_style_group->addAction(m_savedata_load_only_action);
|
|
m_savedata_style_group->addAction(m_savedata_load_and_write_action);
|
|
|
|
m_data_menu->addSeparator();
|
|
|
|
m_savedata_all_wii_saves_action = m_data_menu->addAction(tr("Use All Wii Save Data"));
|
|
m_savedata_all_wii_saves_action->setToolTip(tr(
|
|
"If checked, all Wii saves will be used instead of only the save of the game being started. "
|
|
"Useful when switching games mid-session. Has no effect if No Save Data is selected."));
|
|
m_savedata_all_wii_saves_action->setCheckable(true);
|
|
|
|
m_data_menu->addSeparator();
|
|
|
|
m_sync_codes_action = m_data_menu->addAction(tr("Sync AR/Gecko Codes"));
|
|
m_sync_codes_action->setCheckable(true);
|
|
m_strict_settings_sync_action = m_data_menu->addAction(tr("Strict Settings Sync"));
|
|
m_strict_settings_sync_action->setToolTip(
|
|
tr("This will sync additional graphics settings, and force everyone to the same internal "
|
|
"resolution.\nMay prevent desync in some games that use EFB reads. Please ensure everyone "
|
|
"uses the same video backend."));
|
|
m_strict_settings_sync_action->setCheckable(true);
|
|
|
|
m_network_menu = m_menu_bar->addMenu(tr("Network"));
|
|
m_network_menu->setToolTipsVisible(true);
|
|
m_fixed_delay_action = m_network_menu->addAction(tr("Fair Input Delay"));
|
|
m_fixed_delay_action->setToolTip(
|
|
tr("Each player sends their own inputs to the game, with equal buffer size for all players, "
|
|
"configured by the host.\nSuitable for competitive games where fairness and minimal "
|
|
"latency are most important."));
|
|
m_fixed_delay_action->setCheckable(true);
|
|
m_host_input_authority_action = m_network_menu->addAction(tr("Host Input Authority"));
|
|
m_host_input_authority_action->setToolTip(
|
|
tr("Host has control of sending all inputs to the game, as received from other players, "
|
|
"giving the host zero latency but increasing latency for others.\nSuitable for casual "
|
|
"games with 3+ players, possibly on unstable or high latency connections."));
|
|
m_host_input_authority_action->setCheckable(true);
|
|
m_golf_mode_action = m_network_menu->addAction(tr("Golf Mode"));
|
|
m_golf_mode_action->setToolTip(
|
|
tr("Identical to Host Input Authority, except the \"Host\" (who has zero latency) can be "
|
|
"switched at any time.\nSuitable for turn-based games with timing-sensitive controls, "
|
|
"such as golf."));
|
|
m_golf_mode_action->setCheckable(true);
|
|
|
|
m_network_mode_group = new QActionGroup(this);
|
|
m_network_mode_group->setExclusive(true);
|
|
m_network_mode_group->addAction(m_fixed_delay_action);
|
|
m_network_mode_group->addAction(m_host_input_authority_action);
|
|
m_network_mode_group->addAction(m_golf_mode_action);
|
|
m_fixed_delay_action->setChecked(true);
|
|
|
|
m_game_digest_menu = m_menu_bar->addMenu(tr("Checksum"));
|
|
m_game_digest_menu->addAction(tr("Current game"), this, [this] {
|
|
Settings::Instance().GetNetPlayServer()->ComputeGameDigest(m_current_game_identifier);
|
|
});
|
|
m_game_digest_menu->addAction(tr("Other game..."), this, [this] {
|
|
GameListDialog gld(m_game_list_model, this);
|
|
|
|
SetQWidgetWindowDecorations(&gld);
|
|
if (gld.exec() != QDialog::Accepted)
|
|
return;
|
|
Settings::Instance().GetNetPlayServer()->ComputeGameDigest(
|
|
gld.GetSelectedGame().GetSyncIdentifier());
|
|
});
|
|
m_game_digest_menu->addAction(tr("SD Card"), this, [] {
|
|
Settings::Instance().GetNetPlayServer()->ComputeGameDigest(
|
|
NetPlay::NetPlayClient::GetSDCardIdentifier());
|
|
});
|
|
|
|
m_other_menu = m_menu_bar->addMenu(tr("Other"));
|
|
m_record_input_action = m_other_menu->addAction(tr("Record Inputs"));
|
|
m_record_input_action->setCheckable(true);
|
|
m_golf_mode_overlay_action = m_other_menu->addAction(tr("Show Golf Mode Overlay"));
|
|
m_golf_mode_overlay_action->setCheckable(true);
|
|
m_hide_remote_gbas_action = m_other_menu->addAction(tr("Hide Remote GBAs"));
|
|
m_hide_remote_gbas_action->setCheckable(true);
|
|
|
|
m_game_button->setDefault(false);
|
|
m_game_button->setAutoDefault(false);
|
|
|
|
m_savedata_load_only_action->setChecked(true);
|
|
m_sync_codes_action->setChecked(true);
|
|
|
|
m_main_layout->setMenuBar(m_menu_bar);
|
|
|
|
m_main_layout->addWidget(m_game_button, 0, 0, 1, -1);
|
|
m_main_layout->addWidget(m_splitter, 1, 0, 1, -1);
|
|
|
|
m_splitter->addWidget(m_chat_box);
|
|
m_splitter->addWidget(m_players_box);
|
|
|
|
auto* options_widget = new QGridLayout;
|
|
|
|
options_widget->addWidget(m_start_button, 0, 0, Qt::AlignVCenter);
|
|
options_widget->addWidget(m_buffer_label, 0, 1, Qt::AlignVCenter);
|
|
options_widget->addWidget(m_buffer_size_box, 0, 2, Qt::AlignVCenter);
|
|
options_widget->addWidget(m_quit_button, 0, 3, Qt::AlignVCenter | Qt::AlignRight);
|
|
options_widget->setColumnStretch(3, 1000);
|
|
|
|
m_main_layout->addLayout(options_widget, 2, 0, 1, -1, Qt::AlignRight);
|
|
m_main_layout->setRowStretch(1, 1000);
|
|
|
|
setLayout(m_main_layout);
|
|
}
|
|
|
|
void NetPlayDialog::CreateChatLayout()
|
|
{
|
|
m_chat_box = new QGroupBox(tr("Chat"));
|
|
m_chat_edit = new QTextBrowser;
|
|
m_chat_type_edit = new QLineEdit;
|
|
m_chat_send_button = new QPushButton(tr("Send"));
|
|
|
|
// This button will get re-enabled when something gets entered into the chat box
|
|
m_chat_send_button->setEnabled(false);
|
|
m_chat_send_button->setDefault(false);
|
|
m_chat_send_button->setAutoDefault(false);
|
|
|
|
m_chat_edit->setReadOnly(true);
|
|
|
|
auto* layout = new QGridLayout;
|
|
|
|
layout->addWidget(m_chat_edit, 0, 0, 1, -1);
|
|
layout->addWidget(m_chat_type_edit, 1, 0);
|
|
layout->addWidget(m_chat_send_button, 1, 1);
|
|
|
|
m_chat_box->setLayout(layout);
|
|
}
|
|
|
|
void NetPlayDialog::CreatePlayersLayout()
|
|
{
|
|
m_players_box = new QGroupBox(tr("Players"));
|
|
m_room_box = new QComboBox;
|
|
m_hostcode_label = new QLabel;
|
|
m_hostcode_action_button = new QPushButton(tr("Copy"));
|
|
m_players_list = new QTableWidget;
|
|
m_kick_button = new QPushButton(tr("Kick Player"));
|
|
m_assign_ports_button = new QPushButton(tr("Assign Controller Ports"));
|
|
|
|
m_players_list->setTabKeyNavigation(false);
|
|
m_players_list->setColumnCount(5);
|
|
m_players_list->verticalHeader()->hide();
|
|
m_players_list->setSelectionBehavior(QAbstractItemView::SelectRows);
|
|
m_players_list->horizontalHeader()->setStretchLastSection(true);
|
|
m_players_list->horizontalHeader()->setHighlightSections(false);
|
|
|
|
for (int i = 0; i < 4; i++)
|
|
m_players_list->horizontalHeader()->setSectionResizeMode(i, QHeaderView::ResizeToContents);
|
|
|
|
auto* layout = new QGridLayout;
|
|
|
|
layout->addWidget(m_room_box, 0, 0);
|
|
layout->addWidget(m_hostcode_label, 0, 1);
|
|
layout->addWidget(m_hostcode_action_button, 0, 2);
|
|
layout->addWidget(m_players_list, 1, 0, 1, -1);
|
|
layout->addWidget(m_kick_button, 2, 0, 1, -1);
|
|
layout->addWidget(m_assign_ports_button, 3, 0, 1, -1);
|
|
|
|
m_players_box->setLayout(layout);
|
|
}
|
|
|
|
void NetPlayDialog::ConnectWidgets()
|
|
{
|
|
// Players
|
|
connect(m_room_box, &QComboBox::currentIndexChanged, this, &NetPlayDialog::UpdateGUI);
|
|
connect(m_hostcode_action_button, &QPushButton::clicked, [this] {
|
|
if (m_is_copy_button_retry)
|
|
Common::g_TraversalClient->ReconnectToServer();
|
|
else
|
|
QApplication::clipboard()->setText(m_hostcode_label->text());
|
|
});
|
|
connect(m_players_list, &QTableWidget::itemSelectionChanged, [this] {
|
|
int row = m_players_list->currentRow();
|
|
m_kick_button->setEnabled(row > 0 &&
|
|
!m_players_list->currentItem()->data(Qt::UserRole).isNull());
|
|
});
|
|
connect(m_kick_button, &QPushButton::clicked, [this] {
|
|
auto id = m_players_list->currentItem()->data(Qt::UserRole).toInt();
|
|
Settings::Instance().GetNetPlayServer()->KickPlayer(id);
|
|
});
|
|
connect(m_assign_ports_button, &QPushButton::clicked, [this] {
|
|
SetQWidgetWindowDecorations(m_pad_mapping);
|
|
m_pad_mapping->exec();
|
|
|
|
Settings::Instance().GetNetPlayServer()->SetPadMapping(m_pad_mapping->GetGCPadArray());
|
|
Settings::Instance().GetNetPlayServer()->SetGBAConfig(m_pad_mapping->GetGBAArray(), true);
|
|
Settings::Instance().GetNetPlayServer()->SetWiimoteMapping(m_pad_mapping->GetWiimoteArray());
|
|
});
|
|
|
|
// Chat
|
|
connect(m_chat_send_button, &QPushButton::clicked, this, &NetPlayDialog::OnChat);
|
|
connect(m_chat_type_edit, &QLineEdit::returnPressed, this, &NetPlayDialog::OnChat);
|
|
connect(m_chat_type_edit, &QLineEdit::textChanged, this,
|
|
[this] { m_chat_send_button->setEnabled(!m_chat_type_edit->text().isEmpty()); });
|
|
|
|
// Other
|
|
connect(m_buffer_size_box, &QSpinBox::valueChanged, [this](int value) {
|
|
if (value == m_buffer_size)
|
|
return;
|
|
|
|
auto client = Settings::Instance().GetNetPlayClient();
|
|
auto server = Settings::Instance().GetNetPlayServer();
|
|
if (server && !m_host_input_authority)
|
|
server->AdjustPadBufferSize(value);
|
|
else
|
|
client->AdjustPadBufferSize(value);
|
|
});
|
|
|
|
const auto hia_function = [this](bool enable) {
|
|
if (m_host_input_authority != enable)
|
|
{
|
|
auto server = Settings::Instance().GetNetPlayServer();
|
|
if (server)
|
|
server->SetHostInputAuthority(enable);
|
|
}
|
|
};
|
|
|
|
connect(m_host_input_authority_action, &QAction::toggled, this,
|
|
[hia_function] { hia_function(true); });
|
|
connect(m_golf_mode_action, &QAction::toggled, this, [hia_function] { hia_function(true); });
|
|
connect(m_fixed_delay_action, &QAction::toggled, this, [hia_function] { hia_function(false); });
|
|
|
|
connect(m_start_button, &QPushButton::clicked, this, &NetPlayDialog::OnStart);
|
|
connect(m_quit_button, &QPushButton::clicked, this, &NetPlayDialog::reject);
|
|
|
|
connect(m_game_button, &QPushButton::clicked, [this] {
|
|
GameListDialog gld(m_game_list_model, this);
|
|
SetQWidgetWindowDecorations(&gld);
|
|
if (gld.exec() == QDialog::Accepted)
|
|
{
|
|
Settings& settings = Settings::Instance();
|
|
|
|
const UICommon::GameFile& game = gld.GetSelectedGame();
|
|
const std::string netplay_name = m_game_list_model.GetNetPlayName(game);
|
|
|
|
settings.GetNetPlayServer()->ChangeGame(game.GetSyncIdentifier(), netplay_name);
|
|
Settings::GetQSettings().setValue(QStringLiteral("netplay/hostgame"),
|
|
QString::fromStdString(netplay_name));
|
|
}
|
|
});
|
|
|
|
connect(&Settings::Instance(), &Settings::EmulationStateChanged, this, [this](Core::State state) {
|
|
if (isVisible())
|
|
{
|
|
GameStatusChanged(state != Core::State::Uninitialized);
|
|
if ((state == Core::State::Uninitialized || state == Core::State::Stopping) &&
|
|
!m_got_stop_request)
|
|
{
|
|
Settings::Instance().GetNetPlayClient()->RequestStopGame();
|
|
}
|
|
if (state == Core::State::Uninitialized)
|
|
DisplayMessage(tr("Stopped game"), "red");
|
|
}
|
|
});
|
|
|
|
// SaveSettings() - Save Hosting-Dialog Settings
|
|
|
|
connect(m_buffer_size_box, &QSpinBox::valueChanged, this, &NetPlayDialog::SaveSettings);
|
|
connect(m_savedata_none_action, &QAction::toggled, this, &NetPlayDialog::SaveSettings);
|
|
connect(m_savedata_load_only_action, &QAction::toggled, this, &NetPlayDialog::SaveSettings);
|
|
connect(m_savedata_load_and_write_action, &QAction::toggled, this, &NetPlayDialog::SaveSettings);
|
|
connect(m_savedata_all_wii_saves_action, &QAction::toggled, this, &NetPlayDialog::SaveSettings);
|
|
connect(m_sync_codes_action, &QAction::toggled, this, &NetPlayDialog::SaveSettings);
|
|
connect(m_record_input_action, &QAction::toggled, this, &NetPlayDialog::SaveSettings);
|
|
connect(m_strict_settings_sync_action, &QAction::toggled, this, &NetPlayDialog::SaveSettings);
|
|
connect(m_host_input_authority_action, &QAction::toggled, this, &NetPlayDialog::SaveSettings);
|
|
connect(m_golf_mode_action, &QAction::toggled, this, &NetPlayDialog::SaveSettings);
|
|
connect(m_golf_mode_overlay_action, &QAction::toggled, this, &NetPlayDialog::SaveSettings);
|
|
connect(m_fixed_delay_action, &QAction::toggled, this, &NetPlayDialog::SaveSettings);
|
|
connect(m_hide_remote_gbas_action, &QAction::toggled, this, &NetPlayDialog::SaveSettings);
|
|
}
|
|
|
|
void NetPlayDialog::SendMessage(const std::string& msg)
|
|
{
|
|
Settings::Instance().GetNetPlayClient()->SendChatMessage(msg);
|
|
|
|
DisplayMessage(
|
|
QStringLiteral("%1: %2").arg(QString::fromStdString(m_nickname), QString::fromStdString(msg)),
|
|
"");
|
|
}
|
|
|
|
void NetPlayDialog::OnChat()
|
|
{
|
|
QueueOnObject(this, [this] {
|
|
auto msg = m_chat_type_edit->text().toStdString();
|
|
|
|
if (msg.empty())
|
|
return;
|
|
|
|
m_chat_type_edit->clear();
|
|
|
|
SendMessage(msg);
|
|
});
|
|
}
|
|
|
|
void NetPlayDialog::OnIndexAdded(bool success, const std::string error)
|
|
{
|
|
DisplayMessage(success ? tr("Successfully added to the NetPlay index") :
|
|
tr("Failed to add this session to the NetPlay index: %1")
|
|
.arg(QString::fromStdString(error)),
|
|
success ? "green" : "red");
|
|
}
|
|
|
|
void NetPlayDialog::OnIndexRefreshFailed(const std::string error)
|
|
{
|
|
DisplayMessage(QString::fromStdString(error), "red");
|
|
}
|
|
|
|
void NetPlayDialog::OnStart()
|
|
{
|
|
if (!Settings::Instance().GetNetPlayClient()->DoAllPlayersHaveGame())
|
|
{
|
|
if (ModalMessageBox::question(
|
|
this, tr("Warning"),
|
|
tr("Not all players have the game. Do you really want to start?")) == QMessageBox::No)
|
|
return;
|
|
}
|
|
|
|
if (m_strict_settings_sync_action->isChecked() && Config::Get(Config::GFX_EFB_SCALE) == 0)
|
|
{
|
|
ModalMessageBox::critical(
|
|
this, tr("Error"),
|
|
tr("Auto internal resolution is not allowed in strict sync mode, as it depends on window "
|
|
"size.\n\nPlease select a specific internal resolution."));
|
|
return;
|
|
}
|
|
|
|
const auto game = FindGameFile(m_current_game_identifier);
|
|
if (!game)
|
|
{
|
|
PanicAlertFmtT("Selected game doesn't exist in game list!");
|
|
return;
|
|
}
|
|
|
|
if (Settings::Instance().GetNetPlayServer()->RequestStartGame())
|
|
SetOptionsEnabled(false);
|
|
}
|
|
|
|
void NetPlayDialog::reject()
|
|
{
|
|
if (ModalMessageBox::question(this, tr("Confirmation"),
|
|
tr("Are you sure you want to quit NetPlay?")) == QMessageBox::Yes)
|
|
{
|
|
QDialog::reject();
|
|
}
|
|
}
|
|
|
|
void NetPlayDialog::show(std::string nickname, bool use_traversal)
|
|
{
|
|
m_nickname = nickname;
|
|
m_use_traversal = use_traversal;
|
|
m_buffer_size = 0;
|
|
m_old_player_count = 0;
|
|
|
|
m_room_box->clear();
|
|
m_chat_edit->clear();
|
|
m_chat_type_edit->clear();
|
|
|
|
bool is_hosting = Settings::Instance().GetNetPlayServer() != nullptr;
|
|
|
|
if (is_hosting)
|
|
{
|
|
if (use_traversal)
|
|
m_room_box->addItem(tr("Room ID"));
|
|
m_room_box->addItem(tr("External"));
|
|
|
|
for (const auto& iface : Settings::Instance().GetNetPlayServer()->GetInterfaceSet())
|
|
{
|
|
const auto interface = QString::fromStdString(iface);
|
|
m_room_box->addItem(iface == "!local!" ? tr("Local") : interface, interface);
|
|
}
|
|
}
|
|
|
|
m_data_menu->menuAction()->setVisible(is_hosting);
|
|
m_network_menu->menuAction()->setVisible(is_hosting);
|
|
m_game_digest_menu->menuAction()->setVisible(is_hosting);
|
|
#ifdef HAS_LIBMGBA
|
|
m_hide_remote_gbas_action->setVisible(is_hosting);
|
|
#else
|
|
m_hide_remote_gbas_action->setVisible(false);
|
|
#endif
|
|
m_start_button->setHidden(!is_hosting);
|
|
m_kick_button->setHidden(!is_hosting);
|
|
m_assign_ports_button->setHidden(!is_hosting);
|
|
m_room_box->setHidden(!is_hosting);
|
|
m_hostcode_label->setHidden(!is_hosting);
|
|
m_hostcode_action_button->setHidden(!is_hosting);
|
|
m_game_button->setEnabled(is_hosting);
|
|
m_kick_button->setEnabled(false);
|
|
|
|
SetOptionsEnabled(true);
|
|
|
|
QDialog::show();
|
|
UpdateGUI();
|
|
}
|
|
|
|
void NetPlayDialog::ResetExternalIP()
|
|
{
|
|
m_external_ip_address = Common::Lazy<std::string>([]() -> std::string {
|
|
Common::HttpRequest request;
|
|
// ENet does not support IPv6, so IPv4 has to be used
|
|
request.UseIPv4();
|
|
Common::HttpRequest::Response response =
|
|
request.Get("https://ip.dolphin-emu.org/", {{"X-Is-Dolphin", "1"}});
|
|
|
|
if (response.has_value())
|
|
return std::string(response->begin(), response->end());
|
|
return "";
|
|
});
|
|
}
|
|
|
|
void NetPlayDialog::UpdateDiscordPresence()
|
|
{
|
|
#ifdef USE_DISCORD_PRESENCE
|
|
// both m_current_game and m_player_count need to be set for the status to be displayed correctly
|
|
if (m_player_count == 0 || m_current_game_name.empty())
|
|
return;
|
|
|
|
const auto use_default = [this]() {
|
|
Discord::UpdateDiscordPresence(m_player_count, Discord::SecretType::Empty, "",
|
|
m_current_game_name);
|
|
};
|
|
|
|
if (Core::IsRunning(Core::System::GetInstance()))
|
|
return use_default();
|
|
|
|
if (IsHosting())
|
|
{
|
|
if (Common::g_TraversalClient)
|
|
{
|
|
const auto host_id = Common::g_TraversalClient->GetHostID();
|
|
if (host_id[0] == '\0')
|
|
return use_default();
|
|
|
|
Discord::UpdateDiscordPresence(m_player_count, Discord::SecretType::RoomID,
|
|
std::string(host_id.begin(), host_id.end()),
|
|
m_current_game_name);
|
|
}
|
|
else
|
|
{
|
|
if (m_external_ip_address->empty())
|
|
return use_default();
|
|
const int port = Settings::Instance().GetNetPlayServer()->GetPort();
|
|
|
|
Discord::UpdateDiscordPresence(
|
|
m_player_count, Discord::SecretType::IPAddress,
|
|
Discord::CreateSecretFromIPAddress(*m_external_ip_address, port), m_current_game_name);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
use_default();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void NetPlayDialog::UpdateGUI()
|
|
{
|
|
auto client = Settings::Instance().GetNetPlayClient();
|
|
auto server = Settings::Instance().GetNetPlayServer();
|
|
if (!client)
|
|
return;
|
|
|
|
// Update Player List
|
|
const auto players = client->GetPlayers();
|
|
|
|
if (static_cast<int>(players.size()) != m_player_count && m_player_count != 0)
|
|
QApplication::alert(this);
|
|
|
|
m_player_count = static_cast<int>(players.size());
|
|
|
|
int selection_pid = m_players_list->currentItem() ?
|
|
m_players_list->currentItem()->data(Qt::UserRole).toInt() :
|
|
-1;
|
|
|
|
m_players_list->clear();
|
|
m_players_list->setHorizontalHeaderLabels(
|
|
{tr("Player"), tr("Game Status"), tr("Ping"), tr("Mapping"), tr("Revision")});
|
|
m_players_list->setRowCount(m_player_count);
|
|
|
|
static const std::map<NetPlay::SyncIdentifierComparison, std::pair<QString, QString>>
|
|
player_status{
|
|
{NetPlay::SyncIdentifierComparison::SameGame, {tr("OK"), tr("OK")}},
|
|
{NetPlay::SyncIdentifierComparison::DifferentHash,
|
|
{tr("Wrong hash"),
|
|
tr("Game file has a different hash; right-click it, select Properties, switch to the "
|
|
"Verify tab, and select Verify Integrity to check the hash")}},
|
|
{NetPlay::SyncIdentifierComparison::DifferentDiscNumber,
|
|
{tr("Wrong disc number"), tr("Game has a different disc number")}},
|
|
{NetPlay::SyncIdentifierComparison::DifferentRevision,
|
|
{tr("Wrong revision"), tr("Game has a different revision")}},
|
|
{NetPlay::SyncIdentifierComparison::DifferentRegion,
|
|
{tr("Wrong region"), tr("Game region does not match")}},
|
|
{NetPlay::SyncIdentifierComparison::DifferentGame,
|
|
{tr("Not found"), tr("No matching game was found")}},
|
|
};
|
|
|
|
for (int i = 0; i < m_player_count; i++)
|
|
{
|
|
const auto* p = players[i];
|
|
|
|
auto* name_item = new QTableWidgetItem(QString::fromStdString(p->name));
|
|
name_item->setToolTip(name_item->text());
|
|
const auto& status_info = player_status.contains(p->game_status) ?
|
|
player_status.at(p->game_status) :
|
|
std::make_pair(QStringLiteral("?"), QStringLiteral("?"));
|
|
auto* status_item = new QTableWidgetItem(status_info.first);
|
|
status_item->setToolTip(status_info.second);
|
|
auto* ping_item = new QTableWidgetItem(QStringLiteral("%1 ms").arg(p->ping));
|
|
ping_item->setToolTip(ping_item->text());
|
|
auto* mapping_item =
|
|
new QTableWidgetItem(QString::fromStdString(NetPlay::GetPlayerMappingString(
|
|
p->pid, client->GetPadMapping(), client->GetGBAConfig(), client->GetWiimoteMapping())));
|
|
mapping_item->setToolTip(mapping_item->text());
|
|
auto* revision_item = new QTableWidgetItem(QString::fromStdString(p->revision));
|
|
revision_item->setToolTip(revision_item->text());
|
|
|
|
for (auto* item : {name_item, status_item, ping_item, mapping_item, revision_item})
|
|
{
|
|
item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
|
|
item->setData(Qt::UserRole, static_cast<int>(p->pid));
|
|
}
|
|
|
|
m_players_list->setItem(i, 0, name_item);
|
|
m_players_list->setItem(i, 1, status_item);
|
|
m_players_list->setItem(i, 2, ping_item);
|
|
m_players_list->setItem(i, 3, mapping_item);
|
|
m_players_list->setItem(i, 4, revision_item);
|
|
|
|
if (p->pid == selection_pid)
|
|
m_players_list->selectRow(i);
|
|
}
|
|
|
|
if (m_old_player_count != m_player_count)
|
|
{
|
|
UpdateDiscordPresence();
|
|
m_old_player_count = m_player_count;
|
|
}
|
|
|
|
if (!server)
|
|
return;
|
|
|
|
const bool is_local_ip_selected = m_room_box->currentIndex() > (m_use_traversal ? 1 : 0);
|
|
if (is_local_ip_selected)
|
|
{
|
|
m_hostcode_label->setText(QString::fromStdString(
|
|
server->GetInterfaceHost(m_room_box->currentData().toString().toStdString())));
|
|
m_hostcode_action_button->setEnabled(true);
|
|
m_hostcode_action_button->setText(tr("Copy"));
|
|
m_is_copy_button_retry = false;
|
|
}
|
|
else if (m_use_traversal)
|
|
{
|
|
switch (Common::g_TraversalClient->GetState())
|
|
{
|
|
case Common::TraversalClient::State::Connecting:
|
|
m_hostcode_label->setText(tr("Connecting"));
|
|
m_hostcode_action_button->setEnabled(false);
|
|
m_hostcode_action_button->setText(tr("..."));
|
|
break;
|
|
case Common::TraversalClient::State::Connected:
|
|
{
|
|
if (m_room_box->currentIndex() == 0)
|
|
{
|
|
// Display Room ID.
|
|
const auto host_id = Common::g_TraversalClient->GetHostID();
|
|
m_hostcode_label->setText(
|
|
QString::fromStdString(std::string(host_id.begin(), host_id.end())));
|
|
}
|
|
else
|
|
{
|
|
// Externally mapped IP and port are known when using the traversal server.
|
|
m_hostcode_label->setText(
|
|
InetAddressToString(Common::g_TraversalClient->GetExternalAddress()));
|
|
}
|
|
|
|
m_hostcode_action_button->setEnabled(true);
|
|
m_hostcode_action_button->setText(tr("Copy"));
|
|
m_is_copy_button_retry = false;
|
|
break;
|
|
}
|
|
case Common::TraversalClient::State::Failure:
|
|
m_hostcode_label->setText(tr("Error"));
|
|
m_hostcode_action_button->setText(tr("Retry"));
|
|
m_hostcode_action_button->setEnabled(true);
|
|
m_is_copy_button_retry = true;
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Display External IP.
|
|
if (!m_external_ip_address->empty())
|
|
{
|
|
const int port = Settings::Instance().GetNetPlayServer()->GetPort();
|
|
m_hostcode_label->setText(QStringLiteral("%1:%2").arg(
|
|
QString::fromStdString(*m_external_ip_address), QString::number(port)));
|
|
m_hostcode_action_button->setEnabled(true);
|
|
}
|
|
else
|
|
{
|
|
m_hostcode_label->setText(tr("Unknown"));
|
|
m_hostcode_action_button->setEnabled(false);
|
|
}
|
|
|
|
m_hostcode_action_button->setText(tr("Copy"));
|
|
m_is_copy_button_retry = false;
|
|
}
|
|
}
|
|
|
|
// NetPlayUI methods
|
|
|
|
void NetPlayDialog::BootGame(const std::string& filename,
|
|
std::unique_ptr<BootSessionData> boot_session_data)
|
|
{
|
|
m_got_stop_request = false;
|
|
m_start_game_callback(filename, std::move(boot_session_data));
|
|
}
|
|
|
|
void NetPlayDialog::StopGame()
|
|
{
|
|
if (m_got_stop_request)
|
|
return;
|
|
|
|
m_got_stop_request = true;
|
|
emit Stop();
|
|
}
|
|
|
|
bool NetPlayDialog::IsHosting() const
|
|
{
|
|
return Settings::Instance().GetNetPlayServer() != nullptr;
|
|
}
|
|
|
|
void NetPlayDialog::Update()
|
|
{
|
|
QueueOnObject(this, &NetPlayDialog::UpdateGUI);
|
|
}
|
|
|
|
void NetPlayDialog::DisplayMessage(const QString& msg, const std::string& color, int duration)
|
|
{
|
|
QueueOnObject(m_chat_edit, [this, color, msg] {
|
|
m_chat_edit->append(QStringLiteral("<font color='%1'>%2</font>")
|
|
.arg(QString::fromStdString(color), msg.toHtmlEscaped()));
|
|
});
|
|
|
|
QColor c(color.empty() ? QStringLiteral("white") : QString::fromStdString(color));
|
|
|
|
if (g_ActiveConfig.bShowNetPlayMessages && Core::IsRunning(Core::System::GetInstance()))
|
|
{
|
|
g_netplay_chat_ui->AppendChat(msg.toStdString(),
|
|
{static_cast<float>(c.redF()), static_cast<float>(c.greenF()),
|
|
static_cast<float>(c.blueF())});
|
|
}
|
|
}
|
|
|
|
void NetPlayDialog::AppendChat(const std::string& msg)
|
|
{
|
|
DisplayMessage(QString::fromStdString(msg), "");
|
|
QApplication::alert(this);
|
|
}
|
|
|
|
void NetPlayDialog::OnMsgChangeGame(const NetPlay::SyncIdentifier& sync_identifier,
|
|
const std::string& netplay_name)
|
|
{
|
|
QString qname = QString::fromStdString(netplay_name);
|
|
QueueOnObject(this, [this, qname, netplay_name, &sync_identifier] {
|
|
m_game_button->setText(qname);
|
|
m_current_game_identifier = sync_identifier;
|
|
m_current_game_name = netplay_name;
|
|
UpdateDiscordPresence();
|
|
});
|
|
DisplayMessage(tr("Game changed to \"%1\"").arg(qname), "magenta");
|
|
}
|
|
|
|
void NetPlayDialog::OnMsgChangeGBARom(int pad, const NetPlay::GBAConfig& config)
|
|
{
|
|
if (config.has_rom)
|
|
{
|
|
DisplayMessage(
|
|
tr("GBA%1 ROM changed to \"%2\"").arg(pad + 1).arg(QString::fromStdString(config.title)),
|
|
"magenta");
|
|
}
|
|
else
|
|
{
|
|
DisplayMessage(tr("GBA%1 ROM disabled").arg(pad + 1), "magenta");
|
|
}
|
|
}
|
|
|
|
void NetPlayDialog::GameStatusChanged(bool running)
|
|
{
|
|
QueueOnObject(this, [this, running] { SetOptionsEnabled(!running); });
|
|
}
|
|
|
|
void NetPlayDialog::SetOptionsEnabled(bool enabled)
|
|
{
|
|
if (Settings::Instance().GetNetPlayServer())
|
|
{
|
|
m_start_button->setEnabled(enabled);
|
|
m_game_button->setEnabled(enabled);
|
|
m_savedata_none_action->setEnabled(enabled);
|
|
m_savedata_load_only_action->setEnabled(enabled);
|
|
m_savedata_load_and_write_action->setEnabled(enabled);
|
|
m_savedata_all_wii_saves_action->setEnabled(enabled);
|
|
m_sync_codes_action->setEnabled(enabled);
|
|
m_assign_ports_button->setEnabled(enabled);
|
|
m_strict_settings_sync_action->setEnabled(enabled);
|
|
m_host_input_authority_action->setEnabled(enabled);
|
|
m_golf_mode_action->setEnabled(enabled);
|
|
m_fixed_delay_action->setEnabled(enabled);
|
|
}
|
|
|
|
m_record_input_action->setEnabled(enabled);
|
|
}
|
|
|
|
void NetPlayDialog::OnMsgStartGame()
|
|
{
|
|
DisplayMessage(tr("Started game"), "green");
|
|
|
|
g_netplay_chat_ui =
|
|
std::make_unique<NetPlayChatUI>([this](const std::string& message) { SendMessage(message); });
|
|
|
|
if (m_host_input_authority && Settings::Instance().GetNetPlayClient()->GetNetSettings().golf_mode)
|
|
{
|
|
g_netplay_golf_ui = std::make_unique<NetPlayGolfUI>(Settings::Instance().GetNetPlayClient());
|
|
}
|
|
|
|
QueueOnObject(this, [this] {
|
|
auto client = Settings::Instance().GetNetPlayClient();
|
|
|
|
if (client)
|
|
{
|
|
if (auto game = FindGameFile(m_current_game_identifier))
|
|
client->StartGame(game->GetFilePath());
|
|
else
|
|
PanicAlertFmtT("Selected game doesn't exist in game list!");
|
|
}
|
|
UpdateDiscordPresence();
|
|
});
|
|
}
|
|
|
|
void NetPlayDialog::OnMsgStopGame()
|
|
{
|
|
g_netplay_chat_ui.reset();
|
|
g_netplay_golf_ui.reset();
|
|
QueueOnObject(this, [this] { UpdateDiscordPresence(); });
|
|
}
|
|
|
|
void NetPlayDialog::OnMsgPowerButton()
|
|
{
|
|
if (!Core::IsRunning(Core::System::GetInstance()))
|
|
return;
|
|
QueueOnObject(this, [] { UICommon::TriggerSTMPowerEvent(); });
|
|
}
|
|
|
|
void NetPlayDialog::OnPlayerConnect(const std::string& player)
|
|
{
|
|
DisplayMessage(tr("%1 has joined").arg(QString::fromStdString(player)), "darkcyan");
|
|
}
|
|
|
|
void NetPlayDialog::OnPlayerDisconnect(const std::string& player)
|
|
{
|
|
DisplayMessage(tr("%1 has left").arg(QString::fromStdString(player)), "darkcyan");
|
|
}
|
|
|
|
void NetPlayDialog::OnPadBufferChanged(u32 buffer)
|
|
{
|
|
QueueOnObject(this, [this, buffer] {
|
|
const QSignalBlocker blocker(m_buffer_size_box);
|
|
m_buffer_size_box->setValue(buffer);
|
|
});
|
|
DisplayMessage(m_host_input_authority ? tr("Max buffer size changed to %1").arg(buffer) :
|
|
tr("Buffer size changed to %1").arg(buffer),
|
|
"darkcyan");
|
|
|
|
m_buffer_size = static_cast<int>(buffer);
|
|
}
|
|
|
|
void NetPlayDialog::OnHostInputAuthorityChanged(bool enabled)
|
|
{
|
|
m_host_input_authority = enabled;
|
|
DisplayMessage(enabled ? tr("Host input authority enabled") : tr("Host input authority disabled"),
|
|
"");
|
|
|
|
QueueOnObject(this, [this, enabled] {
|
|
const bool is_hosting = IsHosting();
|
|
const bool enable_buffer = is_hosting != enabled;
|
|
|
|
if (is_hosting)
|
|
{
|
|
m_buffer_size_box->setEnabled(enable_buffer);
|
|
m_buffer_label->setEnabled(enable_buffer);
|
|
m_buffer_size_box->setHidden(false);
|
|
m_buffer_label->setHidden(false);
|
|
}
|
|
else
|
|
{
|
|
m_buffer_size_box->setEnabled(true);
|
|
m_buffer_label->setEnabled(true);
|
|
m_buffer_size_box->setHidden(!enable_buffer);
|
|
m_buffer_label->setHidden(!enable_buffer);
|
|
}
|
|
|
|
m_buffer_label->setText(enabled ? tr("Max Buffer:") : tr("Buffer:"));
|
|
if (enabled)
|
|
{
|
|
const QSignalBlocker blocker(m_buffer_size_box);
|
|
m_buffer_size_box->setValue(Config::Get(Config::NETPLAY_CLIENT_BUFFER_SIZE));
|
|
}
|
|
});
|
|
}
|
|
|
|
void NetPlayDialog::OnDesync(u32 frame, const std::string& player)
|
|
{
|
|
DisplayMessage(tr("Possible desync detected: %1 might have desynced at frame %2")
|
|
.arg(QString::fromStdString(player), QString::number(frame)),
|
|
"red", OSD::Duration::VERY_LONG);
|
|
}
|
|
|
|
void NetPlayDialog::OnConnectionLost()
|
|
{
|
|
DisplayMessage(tr("Lost connection to NetPlay server..."), "red");
|
|
}
|
|
|
|
void NetPlayDialog::OnConnectionError(const std::string& message)
|
|
{
|
|
QueueOnObject(this, [this, message] {
|
|
ModalMessageBox::critical(this, tr("Error"),
|
|
tr("Failed to connect to server: %1").arg(tr(message.c_str())));
|
|
});
|
|
}
|
|
|
|
void NetPlayDialog::OnTraversalError(Common::TraversalClient::FailureReason error)
|
|
{
|
|
QueueOnObject(this, [this, error] {
|
|
switch (error)
|
|
{
|
|
case Common::TraversalClient::FailureReason::BadHost:
|
|
ModalMessageBox::critical(this, tr("Traversal Error"), tr("Couldn't look up central server"));
|
|
QDialog::reject();
|
|
break;
|
|
case Common::TraversalClient::FailureReason::VersionTooOld:
|
|
ModalMessageBox::critical(this, tr("Traversal Error"),
|
|
tr("Dolphin is too old for traversal server"));
|
|
QDialog::reject();
|
|
break;
|
|
case Common::TraversalClient::FailureReason::ServerForgotAboutUs:
|
|
case Common::TraversalClient::FailureReason::SocketSendError:
|
|
case Common::TraversalClient::FailureReason::ResendTimeout:
|
|
UpdateGUI();
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
|
|
void NetPlayDialog::OnTraversalStateChanged(Common::TraversalClient::State state)
|
|
{
|
|
switch (state)
|
|
{
|
|
case Common::TraversalClient::State::Connected:
|
|
case Common::TraversalClient::State::Failure:
|
|
UpdateDiscordPresence();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void NetPlayDialog::OnGameStartAborted()
|
|
{
|
|
QueueOnObject(this, [this] { SetOptionsEnabled(true); });
|
|
}
|
|
|
|
void NetPlayDialog::OnGolferChanged(const bool is_golfer, const std::string& golfer_name)
|
|
{
|
|
if (m_host_input_authority)
|
|
{
|
|
QueueOnObject(this, [this, is_golfer] {
|
|
m_buffer_size_box->setEnabled(!is_golfer);
|
|
m_buffer_label->setEnabled(!is_golfer);
|
|
});
|
|
}
|
|
|
|
if (!golfer_name.empty())
|
|
DisplayMessage(tr("%1 is now golfing").arg(QString::fromStdString(golfer_name)), "");
|
|
}
|
|
|
|
void NetPlayDialog::OnTtlDetermined(u8 ttl)
|
|
{
|
|
DisplayMessage(tr("Using TTL %1 for probe packet").arg(QString::number(ttl)), "");
|
|
}
|
|
|
|
bool NetPlayDialog::IsRecording()
|
|
{
|
|
std::optional<bool> is_recording = RunOnObject(m_record_input_action, &QAction::isChecked);
|
|
if (is_recording)
|
|
return *is_recording;
|
|
return false;
|
|
}
|
|
|
|
std::shared_ptr<const UICommon::GameFile>
|
|
NetPlayDialog::FindGameFile(const NetPlay::SyncIdentifier& sync_identifier,
|
|
NetPlay::SyncIdentifierComparison* found)
|
|
{
|
|
NetPlay::SyncIdentifierComparison temp;
|
|
if (!found)
|
|
found = &temp;
|
|
|
|
*found = NetPlay::SyncIdentifierComparison::DifferentGame;
|
|
|
|
std::optional<std::shared_ptr<const UICommon::GameFile>> game_file =
|
|
RunOnObject(this, [this, &sync_identifier, found] {
|
|
for (int i = 0; i < m_game_list_model.rowCount(QModelIndex()); i++)
|
|
{
|
|
auto file = m_game_list_model.GetGameFile(i);
|
|
*found = std::min(*found, file->CompareSyncIdentifier(sync_identifier));
|
|
if (*found == NetPlay::SyncIdentifierComparison::SameGame)
|
|
return file;
|
|
}
|
|
return static_cast<std::shared_ptr<const UICommon::GameFile>>(nullptr);
|
|
});
|
|
if (game_file)
|
|
return *game_file;
|
|
return nullptr;
|
|
}
|
|
|
|
std::string NetPlayDialog::FindGBARomPath(const std::array<u8, 20>& hash, std::string_view title,
|
|
int device_number)
|
|
{
|
|
#ifdef HAS_LIBMGBA
|
|
auto result = RunOnObject(this, [&, this] {
|
|
std::string rom_path;
|
|
std::array<u8, 20> rom_hash;
|
|
std::string rom_title;
|
|
for (size_t i = device_number; i < static_cast<size_t>(device_number) + 4; ++i)
|
|
{
|
|
rom_path = Config::Get(Config::MAIN_GBA_ROM_PATHS[i % 4]);
|
|
if (!rom_path.empty() && HW::GBA::Core::GetRomInfo(rom_path.c_str(), rom_hash, rom_title) &&
|
|
rom_hash == hash && rom_title == title)
|
|
{
|
|
return rom_path;
|
|
}
|
|
}
|
|
while (!(rom_path = GameCubePane::GetOpenGBARom(title)).empty())
|
|
{
|
|
if (HW::GBA::Core::GetRomInfo(rom_path.c_str(), rom_hash, rom_title))
|
|
{
|
|
if (rom_hash == hash && rom_title == title)
|
|
return rom_path;
|
|
ModalMessageBox::critical(
|
|
this, tr("Error"),
|
|
QString::fromStdString(Common::FmtFormatT(
|
|
"Mismatched ROMs\n"
|
|
"Selected: {0}\n- Title: {1}\n- Hash: {2:02X}\n"
|
|
"Expected:\n- Title: {3}\n- Hash: {4:02X}",
|
|
rom_path, rom_title, fmt::join(rom_hash, ""), title, fmt::join(hash, ""))));
|
|
}
|
|
else
|
|
{
|
|
ModalMessageBox::critical(
|
|
this, tr("Error"), tr("%1 is not a valid ROM").arg(QString::fromStdString(rom_path)));
|
|
}
|
|
}
|
|
return std::string();
|
|
});
|
|
if (result)
|
|
return *result;
|
|
#endif
|
|
return {};
|
|
}
|
|
|
|
void NetPlayDialog::LoadSettings()
|
|
{
|
|
const int buffer_size = Config::Get(Config::NETPLAY_BUFFER_SIZE);
|
|
const bool savedata_load = Config::Get(Config::NETPLAY_SAVEDATA_LOAD);
|
|
const bool savedata_write = Config::Get(Config::NETPLAY_SAVEDATA_WRITE);
|
|
const bool sync_all_wii_saves = Config::Get(Config::NETPLAY_SAVEDATA_SYNC_ALL_WII);
|
|
const bool sync_codes = Config::Get(Config::NETPLAY_SYNC_CODES);
|
|
const bool record_inputs = Config::Get(Config::NETPLAY_RECORD_INPUTS);
|
|
const bool strict_settings_sync = Config::Get(Config::NETPLAY_STRICT_SETTINGS_SYNC);
|
|
const bool golf_mode_overlay = Config::Get(Config::NETPLAY_GOLF_MODE_OVERLAY);
|
|
const bool hide_remote_gbas = Config::Get(Config::NETPLAY_HIDE_REMOTE_GBAS);
|
|
|
|
m_buffer_size_box->setValue(buffer_size);
|
|
|
|
if (!savedata_load)
|
|
m_savedata_none_action->setChecked(true);
|
|
else if (!savedata_write)
|
|
m_savedata_load_only_action->setChecked(true);
|
|
else
|
|
m_savedata_load_and_write_action->setChecked(true);
|
|
m_savedata_all_wii_saves_action->setChecked(sync_all_wii_saves);
|
|
|
|
m_sync_codes_action->setChecked(sync_codes);
|
|
m_record_input_action->setChecked(record_inputs);
|
|
m_strict_settings_sync_action->setChecked(strict_settings_sync);
|
|
m_golf_mode_overlay_action->setChecked(golf_mode_overlay);
|
|
m_hide_remote_gbas_action->setChecked(hide_remote_gbas);
|
|
|
|
const std::string network_mode = Config::Get(Config::NETPLAY_NETWORK_MODE);
|
|
|
|
if (network_mode == "fixeddelay")
|
|
{
|
|
m_fixed_delay_action->setChecked(true);
|
|
}
|
|
else if (network_mode == "hostinputauthority")
|
|
{
|
|
m_host_input_authority_action->setChecked(true);
|
|
}
|
|
else if (network_mode == "golf")
|
|
{
|
|
m_golf_mode_action->setChecked(true);
|
|
}
|
|
else
|
|
{
|
|
WARN_LOG_FMT(NETPLAY, "Unknown network mode '{}', using 'fixeddelay'", network_mode);
|
|
m_fixed_delay_action->setChecked(true);
|
|
}
|
|
}
|
|
|
|
void NetPlayDialog::SaveSettings()
|
|
{
|
|
Config::ConfigChangeCallbackGuard config_guard;
|
|
|
|
if (m_host_input_authority)
|
|
Config::SetBase(Config::NETPLAY_CLIENT_BUFFER_SIZE, m_buffer_size_box->value());
|
|
else
|
|
Config::SetBase(Config::NETPLAY_BUFFER_SIZE, m_buffer_size_box->value());
|
|
|
|
const bool write_savedata = m_savedata_load_and_write_action->isChecked();
|
|
const bool load_savedata = write_savedata || m_savedata_load_only_action->isChecked();
|
|
Config::SetBase(Config::NETPLAY_SAVEDATA_LOAD, load_savedata);
|
|
Config::SetBase(Config::NETPLAY_SAVEDATA_WRITE, write_savedata);
|
|
|
|
Config::SetBase(Config::NETPLAY_SAVEDATA_SYNC_ALL_WII,
|
|
m_savedata_all_wii_saves_action->isChecked());
|
|
Config::SetBase(Config::NETPLAY_SYNC_CODES, m_sync_codes_action->isChecked());
|
|
Config::SetBase(Config::NETPLAY_RECORD_INPUTS, m_record_input_action->isChecked());
|
|
Config::SetBase(Config::NETPLAY_STRICT_SETTINGS_SYNC, m_strict_settings_sync_action->isChecked());
|
|
Config::SetBase(Config::NETPLAY_GOLF_MODE_OVERLAY, m_golf_mode_overlay_action->isChecked());
|
|
Config::SetBase(Config::NETPLAY_HIDE_REMOTE_GBAS, m_hide_remote_gbas_action->isChecked());
|
|
|
|
std::string network_mode;
|
|
if (m_fixed_delay_action->isChecked())
|
|
{
|
|
network_mode = "fixeddelay";
|
|
}
|
|
else if (m_host_input_authority_action->isChecked())
|
|
{
|
|
network_mode = "hostinputauthority";
|
|
}
|
|
else if (m_golf_mode_action->isChecked())
|
|
{
|
|
network_mode = "golf";
|
|
}
|
|
|
|
Config::SetBase(Config::NETPLAY_NETWORK_MODE, network_mode);
|
|
}
|
|
|
|
void NetPlayDialog::ShowGameDigestDialog(const std::string& title)
|
|
{
|
|
QueueOnObject(this, [this, title] {
|
|
m_game_digest_menu->setEnabled(false);
|
|
|
|
if (m_game_digest_dialog->isVisible())
|
|
m_game_digest_dialog->close();
|
|
|
|
m_game_digest_dialog->show(QString::fromStdString(title));
|
|
});
|
|
}
|
|
|
|
void NetPlayDialog::SetGameDigestProgress(int pid, int progress)
|
|
{
|
|
QueueOnObject(this, [this, pid, progress] {
|
|
if (m_game_digest_dialog->isVisible())
|
|
m_game_digest_dialog->SetProgress(pid, progress);
|
|
});
|
|
}
|
|
|
|
void NetPlayDialog::SetGameDigestResult(int pid, const std::string& result)
|
|
{
|
|
QueueOnObject(this, [this, pid, result] {
|
|
m_game_digest_dialog->SetResult(pid, result);
|
|
m_game_digest_menu->setEnabled(true);
|
|
});
|
|
}
|
|
|
|
void NetPlayDialog::AbortGameDigest()
|
|
{
|
|
QueueOnObject(this, [this] {
|
|
m_game_digest_dialog->close();
|
|
m_game_digest_menu->setEnabled(true);
|
|
});
|
|
}
|
|
|
|
void NetPlayDialog::ShowChunkedProgressDialog(const std::string& title, const u64 data_size,
|
|
const std::vector<int>& players)
|
|
{
|
|
QueueOnObject(this, [this, title, data_size, players] {
|
|
if (m_chunked_progress_dialog->isVisible())
|
|
m_chunked_progress_dialog->done(QDialog::Accepted);
|
|
|
|
m_chunked_progress_dialog->show(QString::fromStdString(title), data_size, players);
|
|
});
|
|
}
|
|
|
|
void NetPlayDialog::HideChunkedProgressDialog()
|
|
{
|
|
QueueOnObject(this, [this] { m_chunked_progress_dialog->done(QDialog::Accepted); });
|
|
}
|
|
|
|
void NetPlayDialog::SetChunkedProgress(const int pid, const u64 progress)
|
|
{
|
|
QueueOnObject(this, [this, pid, progress] {
|
|
if (m_chunked_progress_dialog->isVisible())
|
|
m_chunked_progress_dialog->SetProgress(pid, progress);
|
|
});
|
|
}
|
|
|
|
void NetPlayDialog::SetHostWiiSyncData(std::vector<u64> titles, std::string redirect_folder)
|
|
{
|
|
auto client = Settings::Instance().GetNetPlayClient();
|
|
if (client)
|
|
client->SetWiiSyncData(nullptr, std::move(titles), std::move(redirect_folder));
|
|
}
|