2017-07-21 20:48:21 +00:00
|
|
|
// Copyright 2017 Dolphin Emulator Project
|
2021-07-05 01:22:19 +00:00
|
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
2017-07-21 20:48:21 +00:00
|
|
|
|
2018-07-06 22:40:15 +00:00
|
|
|
#include "DolphinQt/NetPlay/NetPlayDialog.h"
|
2017-07-21 20:48:21 +00:00
|
|
|
|
2018-05-10 18:29:51 +00:00
|
|
|
#include <QAction>
|
2021-01-13 08:55:52 +00:00
|
|
|
#include <QActionGroup>
|
2017-07-21 20:48:21 +00:00
|
|
|
#include <QApplication>
|
|
|
|
#include <QClipboard>
|
|
|
|
#include <QComboBox>
|
2021-07-04 11:33:58 +00:00
|
|
|
#include <QFileDialog>
|
2017-07-21 20:48:21 +00:00
|
|
|
#include <QGridLayout>
|
|
|
|
#include <QGroupBox>
|
2018-05-12 13:13:30 +00:00
|
|
|
#include <QHeaderView>
|
2017-07-21 20:48:21 +00:00
|
|
|
#include <QLabel>
|
|
|
|
#include <QLineEdit>
|
2018-05-10 18:29:51 +00:00
|
|
|
#include <QMenu>
|
2017-07-21 20:48:21 +00:00
|
|
|
#include <QPushButton>
|
NetPlay host input authority mode
Currently, each player buffers their own inputs and sends them to the
host. The host then relays those inputs to everyone else. Every player
waits on inputs from all players to be buffered before continuing. What
this means is all clients run in lockstep, and the total latency of
inputs cannot be lower than the sum of the 2 highest client ping times
in the game (in 3+ player sessions with people across the world, the
latency can be very high).
Host input authority mode changes it so players no longer buffer their
own inputs, and only send them to the host. The host stores only the
most recent input received from a player. The host then sends inputs
for all pads at the SI poll interval, similar to the existing code. If
a player sends inputs to slowly, their last received input is simply
sent again. If they send too quickly, inputs are dropped. This means
that the host has full control over what inputs are actually read by
the game, hence the name of the mode. Also, because the rate at which
inputs are received by SI is decoupled from the rate at which players
are sending inputs, clients are no longer dependent on each other. They
only care what the host is doing. This means that they can set their
buffer individually based on their latency to the host, rather than the
highest latency between any 2 players, allowing someone with lower ping
to the host to have less latency than someone else.
This is a catch to this: as a necessity of how the host's input sending
works, the host has 0 latency. There isn't a good way to fix this, as
input delay is now solely dependent on the real latency to the host's
server. Having differing latency between players would be considered
unfair for competitive play, but for casual play we don't really care.
For this reason though, combined with the potential for a few inputs to
be dropped on a bad connection, the old mode will remain and this new
mode is entirely optional.
2018-08-24 08:17:18 +00:00
|
|
|
#include <QSignalBlocker>
|
2017-07-21 20:48:21 +00:00
|
|
|
#include <QSpinBox>
|
2018-05-10 17:38:58 +00:00
|
|
|
#include <QSplitter>
|
2018-05-12 13:13:30 +00:00
|
|
|
#include <QTableWidget>
|
2018-05-10 17:26:42 +00:00
|
|
|
#include <QTextBrowser>
|
2017-07-21 20:48:21 +00:00
|
|
|
|
2020-06-07 20:58:03 +00:00
|
|
|
#include <algorithm>
|
2017-07-21 20:48:21 +00:00
|
|
|
#include <sstream>
|
|
|
|
|
|
|
|
#include "Common/CommonPaths.h"
|
2017-08-01 14:37:42 +00:00
|
|
|
#include "Common/Config/Config.h"
|
2018-07-03 21:50:08 +00:00
|
|
|
#include "Common/HttpRequest.h"
|
2019-05-30 21:58:31 +00:00
|
|
|
#include "Common/Logging/Log.h"
|
2017-07-21 20:48:21 +00:00
|
|
|
#include "Common/TraversalClient.h"
|
2018-05-28 01:48:04 +00:00
|
|
|
|
2021-11-20 18:59:14 +00:00
|
|
|
#include "Core/Boot/Boot.h"
|
2018-07-19 22:10:37 +00:00
|
|
|
#include "Core/Config/GraphicsSettings.h"
|
|
|
|
#include "Core/Config/MainSettings.h"
|
2018-07-20 22:27:43 +00:00
|
|
|
#include "Core/Config/NetplaySettings.h"
|
2017-07-21 20:48:21 +00:00
|
|
|
#include "Core/ConfigManager.h"
|
|
|
|
#include "Core/Core.h"
|
2021-07-04 11:33:58 +00:00
|
|
|
#ifdef HAS_LIBMGBA
|
|
|
|
#include "Core/HW/GBACore.h"
|
|
|
|
#endif
|
2021-11-20 20:03:34 +00:00
|
|
|
#include "Core/IOS/FS/FileSystem.h"
|
2017-07-21 20:48:21 +00:00
|
|
|
#include "Core/NetPlayServer.h"
|
2020-06-07 20:58:03 +00:00
|
|
|
#include "Core/SyncIdentifier.h"
|
2018-05-28 01:48:04 +00:00
|
|
|
|
2018-10-18 08:33:05 +00:00
|
|
|
#include "DolphinQt/NetPlay/ChunkedProgressDialog.h"
|
2022-07-28 01:43:16 +00:00
|
|
|
#include "DolphinQt/NetPlay/GameDigestDialog.h"
|
2018-07-06 22:40:15 +00:00
|
|
|
#include "DolphinQt/NetPlay/GameListDialog.h"
|
|
|
|
#include "DolphinQt/NetPlay/PadMappingDialog.h"
|
2019-03-04 19:49:00 +00:00
|
|
|
#include "DolphinQt/QtUtils/ModalMessageBox.h"
|
2018-07-06 22:40:15 +00:00
|
|
|
#include "DolphinQt/QtUtils/QueueOnObject.h"
|
|
|
|
#include "DolphinQt/QtUtils/RunOnObject.h"
|
|
|
|
#include "DolphinQt/Resources.h"
|
|
|
|
#include "DolphinQt/Settings.h"
|
2021-07-04 11:33:58 +00:00
|
|
|
#include "DolphinQt/Settings/GameCubePane.h"
|
2018-05-28 01:48:04 +00:00
|
|
|
|
2018-07-03 21:50:08 +00:00
|
|
|
#include "UICommon/DiscordPresence.h"
|
2018-07-19 22:10:37 +00:00
|
|
|
#include "UICommon/GameFile.h"
|
2018-11-11 03:37:49 +00:00
|
|
|
#include "UICommon/UICommon.h"
|
2018-07-19 22:10:37 +00:00
|
|
|
|
2019-03-17 00:09:06 +00:00
|
|
|
#include "VideoCommon/NetPlayChatUI.h"
|
2019-04-02 21:13:42 +00:00
|
|
|
#include "VideoCommon/NetPlayGolfUI.h"
|
2017-07-21 20:48:21 +00:00
|
|
|
#include "VideoCommon/VideoConfig.h"
|
|
|
|
|
2022-03-04 04:08:55 +00:00
|
|
|
namespace
|
|
|
|
{
|
2023-04-24 12:29:12 +00:00
|
|
|
QString InetAddressToString(const Common::TraversalInetAddress& addr)
|
2022-03-04 04:08:55 +00:00
|
|
|
{
|
|
|
|
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
|
|
|
|
|
2021-11-20 18:59:14 +00:00
|
|
|
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))
|
2017-07-21 20:48:21 +00:00
|
|
|
{
|
|
|
|
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
|
|
|
|
|
2018-06-06 01:50:17 +00:00
|
|
|
setWindowTitle(tr("NetPlay"));
|
2018-07-04 20:41:56 +00:00
|
|
|
setWindowIcon(Resources::GetAppIcon());
|
2017-07-21 20:48:21 +00:00
|
|
|
|
|
|
|
m_pad_mapping = new PadMappingDialog(this);
|
2022-07-28 01:43:16 +00:00
|
|
|
m_game_digest_dialog = new GameDigestDialog(this);
|
2018-10-18 08:33:05 +00:00
|
|
|
m_chunked_progress_dialog = new ChunkedProgressDialog(this);
|
2017-07-21 20:48:21 +00:00
|
|
|
|
2018-11-15 05:58:07 +00:00
|
|
|
ResetExternalIP();
|
2017-07-21 20:48:21 +00:00
|
|
|
CreateChatLayout();
|
|
|
|
CreatePlayersLayout();
|
|
|
|
CreateMainLayout();
|
2019-05-30 21:44:02 +00:00
|
|
|
LoadSettings();
|
2017-07-21 20:48:21 +00:00
|
|
|
ConnectWidgets();
|
2018-05-10 17:38:58 +00:00
|
|
|
|
|
|
|
auto& settings = Settings::Instance().GetQSettings();
|
|
|
|
|
2018-05-10 17:42:15 +00:00
|
|
|
restoreGeometry(settings.value(QStringLiteral("netplaydialog/geometry")).toByteArray());
|
2018-05-10 17:38:58 +00:00
|
|
|
m_splitter->restoreState(settings.value(QStringLiteral("netplaydialog/splitter")).toByteArray());
|
|
|
|
}
|
|
|
|
|
|
|
|
NetPlayDialog::~NetPlayDialog()
|
|
|
|
{
|
|
|
|
auto& settings = Settings::Instance().GetQSettings();
|
|
|
|
|
2018-05-10 17:42:15 +00:00
|
|
|
settings.setValue(QStringLiteral("netplaydialog/geometry"), saveGeometry());
|
2018-05-10 17:38:58 +00:00
|
|
|
settings.setValue(QStringLiteral("netplaydialog/splitter"), m_splitter->saveState());
|
2017-07-21 20:48:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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"));
|
2018-05-10 17:38:58 +00:00
|
|
|
m_splitter = new QSplitter(Qt::Horizontal);
|
2019-03-01 04:49:03 +00:00
|
|
|
m_menu_bar = new QMenuBar(this);
|
|
|
|
|
|
|
|
m_data_menu = m_menu_bar->addMenu(tr("Data"));
|
2019-05-30 21:58:31 +00:00
|
|
|
m_data_menu->setToolTipsVisible(true);
|
2022-09-11 00:35:20 +00:00
|
|
|
|
|
|
|
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();
|
|
|
|
|
2019-03-01 04:49:03 +00:00
|
|
|
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"));
|
2019-05-30 21:58:31 +00:00
|
|
|
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."));
|
2019-03-01 04:49:03 +00:00
|
|
|
m_strict_settings_sync_action->setCheckable(true);
|
|
|
|
|
|
|
|
m_network_menu = m_menu_bar->addMenu(tr("Network"));
|
2019-05-30 21:58:31 +00:00
|
|
|
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);
|
2019-03-01 04:49:03 +00:00
|
|
|
m_host_input_authority_action = m_network_menu->addAction(tr("Host Input Authority"));
|
2019-05-30 21:58:31 +00:00
|
|
|
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."));
|
2019-03-01 04:49:03 +00:00
|
|
|
m_host_input_authority_action->setCheckable(true);
|
2019-04-02 12:08:27 +00:00
|
|
|
m_golf_mode_action = m_network_menu->addAction(tr("Golf Mode"));
|
2019-05-30 21:58:31 +00:00
|
|
|
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."));
|
2019-04-02 12:08:27 +00:00
|
|
|
m_golf_mode_action->setCheckable(true);
|
2019-03-01 04:49:03 +00:00
|
|
|
|
2019-05-30 21:58:31 +00:00
|
|
|
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);
|
|
|
|
|
2022-07-28 01:43:16 +00:00
|
|
|
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);
|
2019-04-23 20:37:22 +00:00
|
|
|
});
|
2022-07-28 01:43:16 +00:00
|
|
|
m_game_digest_menu->addAction(tr("Other game..."), this, [this] {
|
2019-01-17 22:28:07 +00:00
|
|
|
GameListDialog gld(m_game_list_model, this);
|
2019-04-23 20:37:22 +00:00
|
|
|
|
|
|
|
if (gld.exec() != QDialog::Accepted)
|
|
|
|
return;
|
2022-07-28 01:43:16 +00:00
|
|
|
Settings::Instance().GetNetPlayServer()->ComputeGameDigest(
|
|
|
|
gld.GetSelectedGame().GetSyncIdentifier());
|
2020-06-07 20:58:03 +00:00
|
|
|
});
|
2022-07-28 01:43:16 +00:00
|
|
|
m_game_digest_menu->addAction(tr("SD Card"), this, [] {
|
|
|
|
Settings::Instance().GetNetPlayServer()->ComputeGameDigest(
|
2020-06-07 20:58:03 +00:00
|
|
|
NetPlay::NetPlayClient::GetSDCardIdentifier());
|
2019-04-23 20:37:22 +00:00
|
|
|
});
|
|
|
|
|
2019-03-01 04:49:03 +00:00
|
|
|
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);
|
2019-04-02 21:13:42 +00:00
|
|
|
m_golf_mode_overlay_action = m_other_menu->addAction(tr("Show Golf Mode Overlay"));
|
|
|
|
m_golf_mode_overlay_action->setCheckable(true);
|
2021-07-04 11:33:58 +00:00
|
|
|
m_hide_remote_gbas_action = m_other_menu->addAction(tr("Hide Remote GBAs"));
|
|
|
|
m_hide_remote_gbas_action->setCheckable(true);
|
2017-07-21 20:48:21 +00:00
|
|
|
|
|
|
|
m_game_button->setDefault(false);
|
|
|
|
m_game_button->setAutoDefault(false);
|
|
|
|
|
2022-09-11 00:35:20 +00:00
|
|
|
m_savedata_load_only_action->setChecked(true);
|
2019-03-01 04:49:03 +00:00
|
|
|
m_sync_codes_action->setChecked(true);
|
2018-07-04 21:01:50 +00:00
|
|
|
|
2019-03-01 04:49:03 +00:00
|
|
|
m_main_layout->setMenuBar(m_menu_bar);
|
2018-06-29 20:48:30 +00:00
|
|
|
|
2019-04-23 20:37:22 +00:00
|
|
|
m_main_layout->addWidget(m_game_button, 0, 0, 1, -1);
|
2018-05-10 17:38:58 +00:00
|
|
|
m_main_layout->addWidget(m_splitter, 1, 0, 1, -1);
|
|
|
|
|
|
|
|
m_splitter->addWidget(m_chat_box);
|
|
|
|
m_splitter->addWidget(m_players_box);
|
2017-07-21 20:48:21 +00:00
|
|
|
|
2018-07-19 22:10:37 +00:00
|
|
|
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);
|
2019-03-01 04:49:03 +00:00
|
|
|
options_widget->addWidget(m_quit_button, 0, 3, Qt::AlignVCenter | Qt::AlignRight);
|
2018-07-19 22:10:37 +00:00
|
|
|
options_widget->setColumnStretch(3, 1000);
|
|
|
|
|
2017-07-21 20:48:21 +00:00
|
|
|
m_main_layout->addLayout(options_widget, 2, 0, 1, -1, Qt::AlignRight);
|
2018-07-19 22:10:37 +00:00
|
|
|
m_main_layout->setRowStretch(1, 1000);
|
2017-07-21 20:48:21 +00:00
|
|
|
|
|
|
|
setLayout(m_main_layout);
|
|
|
|
}
|
|
|
|
|
|
|
|
void NetPlayDialog::CreateChatLayout()
|
|
|
|
{
|
|
|
|
m_chat_box = new QGroupBox(tr("Chat"));
|
2018-05-10 17:26:42 +00:00
|
|
|
m_chat_edit = new QTextBrowser;
|
2017-07-21 20:48:21 +00:00
|
|
|
m_chat_type_edit = new QLineEdit;
|
|
|
|
m_chat_send_button = new QPushButton(tr("Send"));
|
|
|
|
|
2019-03-16 14:33:38 +00:00
|
|
|
// This button will get re-enabled when something gets entered into the chat box
|
|
|
|
m_chat_send_button->setEnabled(false);
|
2017-07-21 20:48:21 +00:00
|
|
|
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"));
|
2018-05-12 13:13:30 +00:00
|
|
|
m_players_list = new QTableWidget;
|
2017-07-21 20:48:21 +00:00
|
|
|
m_kick_button = new QPushButton(tr("Kick Player"));
|
|
|
|
m_assign_ports_button = new QPushButton(tr("Assign Controller Ports"));
|
|
|
|
|
2020-02-07 02:34:27 +00:00
|
|
|
m_players_list->setTabKeyNavigation(false);
|
2018-05-12 13:13:30 +00:00
|
|
|
m_players_list->setColumnCount(5);
|
|
|
|
m_players_list->verticalHeader()->hide();
|
|
|
|
m_players_list->setSelectionBehavior(QAbstractItemView::SelectRows);
|
|
|
|
m_players_list->horizontalHeader()->setStretchLastSection(true);
|
2019-03-16 14:44:03 +00:00
|
|
|
m_players_list->horizontalHeader()->setHighlightSections(false);
|
2018-05-12 13:13:30 +00:00
|
|
|
|
|
|
|
for (int i = 0; i < 4; i++)
|
|
|
|
m_players_list->horizontalHeader()->setSectionResizeMode(i, QHeaderView::ResizeToContents);
|
|
|
|
|
2017-07-21 20:48:21 +00:00
|
|
|
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
|
2019-07-30 13:35:46 +00:00
|
|
|
connect(m_room_box, qOverload<int>(&QComboBox::currentIndexChanged), this,
|
2017-07-21 20:48:21 +00:00
|
|
|
&NetPlayDialog::UpdateGUI);
|
|
|
|
connect(m_hostcode_action_button, &QPushButton::clicked, [this] {
|
2022-03-04 04:08:55 +00:00
|
|
|
if (m_is_copy_button_retry)
|
2023-04-24 12:15:55 +00:00
|
|
|
Common::g_TraversalClient->ReconnectToServer();
|
2017-07-21 20:48:21 +00:00
|
|
|
else
|
|
|
|
QApplication::clipboard()->setText(m_hostcode_label->text());
|
|
|
|
});
|
2018-05-12 13:13:30 +00:00
|
|
|
connect(m_players_list, &QTableWidget::itemSelectionChanged, [this] {
|
2017-07-21 20:48:21 +00:00
|
|
|
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] {
|
|
|
|
m_pad_mapping->exec();
|
|
|
|
|
|
|
|
Settings::Instance().GetNetPlayServer()->SetPadMapping(m_pad_mapping->GetGCPadArray());
|
2021-07-04 11:33:58 +00:00
|
|
|
Settings::Instance().GetNetPlayServer()->SetGBAConfig(m_pad_mapping->GetGBAArray(), true);
|
2017-07-21 20:48:21 +00:00
|
|
|
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);
|
2019-03-16 14:33:38 +00:00
|
|
|
connect(m_chat_type_edit, &QLineEdit::textChanged, this,
|
|
|
|
[this] { m_chat_send_button->setEnabled(!m_chat_type_edit->text().isEmpty()); });
|
2017-07-21 20:48:21 +00:00
|
|
|
|
|
|
|
// Other
|
2019-07-30 13:35:46 +00:00
|
|
|
connect(m_buffer_size_box, qOverload<int>(&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);
|
|
|
|
});
|
2017-07-21 20:48:21 +00:00
|
|
|
|
2019-05-30 21:58:31 +00:00
|
|
|
const auto hia_function = [this](bool enable) {
|
|
|
|
if (m_host_input_authority != enable)
|
|
|
|
{
|
|
|
|
auto server = Settings::Instance().GetNetPlayServer();
|
|
|
|
if (server)
|
|
|
|
server->SetHostInputAuthority(enable);
|
|
|
|
}
|
|
|
|
};
|
2019-04-02 12:08:27 +00:00
|
|
|
|
2019-05-30 21:58:31 +00:00
|
|
|
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); });
|
NetPlay host input authority mode
Currently, each player buffers their own inputs and sends them to the
host. The host then relays those inputs to everyone else. Every player
waits on inputs from all players to be buffered before continuing. What
this means is all clients run in lockstep, and the total latency of
inputs cannot be lower than the sum of the 2 highest client ping times
in the game (in 3+ player sessions with people across the world, the
latency can be very high).
Host input authority mode changes it so players no longer buffer their
own inputs, and only send them to the host. The host stores only the
most recent input received from a player. The host then sends inputs
for all pads at the SI poll interval, similar to the existing code. If
a player sends inputs to slowly, their last received input is simply
sent again. If they send too quickly, inputs are dropped. This means
that the host has full control over what inputs are actually read by
the game, hence the name of the mode. Also, because the rate at which
inputs are received by SI is decoupled from the rate at which players
are sending inputs, clients are no longer dependent on each other. They
only care what the host is doing. This means that they can set their
buffer individually based on their latency to the host, rather than the
highest latency between any 2 players, allowing someone with lower ping
to the host to have less latency than someone else.
This is a catch to this: as a necessity of how the host's input sending
works, the host has 0 latency. There isn't a good way to fix this, as
input delay is now solely dependent on the real latency to the host's
server. Having differing latency between players would be considered
unfair for competitive play, but for casual play we don't really care.
For this reason though, combined with the potential for a few inputs to
be dropped on a bad connection, the old mode will remain and this new
mode is entirely optional.
2018-08-24 08:17:18 +00:00
|
|
|
|
2017-07-21 20:48:21 +00:00
|
|
|
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] {
|
2019-01-17 22:28:07 +00:00
|
|
|
GameListDialog gld(m_game_list_model, this);
|
2017-07-21 20:48:21 +00:00
|
|
|
if (gld.exec() == QDialog::Accepted)
|
|
|
|
{
|
2020-06-10 16:49:22 +00:00
|
|
|
Settings& settings = Settings::Instance();
|
|
|
|
|
2020-06-07 20:58:03 +00:00
|
|
|
const UICommon::GameFile& game = gld.GetSelectedGame();
|
2019-01-17 22:28:07 +00:00
|
|
|
const std::string netplay_name = m_game_list_model.GetNetPlayName(game);
|
2020-06-10 16:49:22 +00:00
|
|
|
|
|
|
|
settings.GetNetPlayServer()->ChangeGame(game.GetSyncIdentifier(), netplay_name);
|
2020-06-07 20:58:03 +00:00
|
|
|
Settings::GetQSettings().setValue(QStringLiteral("netplay/hostgame"),
|
|
|
|
QString::fromStdString(netplay_name));
|
2017-07-21 20:48:21 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2022-08-08 01:25:19 +00:00
|
|
|
connect(&Settings::Instance(), &Settings::EmulationStateChanged, this, [this](Core::State state) {
|
2018-07-03 23:02:13 +00:00
|
|
|
if (isVisible())
|
|
|
|
{
|
|
|
|
GameStatusChanged(state != Core::State::Uninitialized);
|
2018-11-30 06:20:51 +00:00
|
|
|
if ((state == Core::State::Uninitialized || state == Core::State::Stopping) &&
|
|
|
|
!m_got_stop_request)
|
|
|
|
{
|
|
|
|
Settings::Instance().GetNetPlayClient()->RequestStopGame();
|
|
|
|
}
|
2018-07-03 23:02:13 +00:00
|
|
|
if (state == Core::State::Uninitialized)
|
|
|
|
DisplayMessage(tr("Stopped game"), "red");
|
|
|
|
}
|
2017-07-21 20:48:21 +00:00
|
|
|
});
|
2018-10-09 22:52:19 +00:00
|
|
|
|
|
|
|
// SaveSettings() - Save Hosting-Dialog Settings
|
|
|
|
|
2019-07-30 13:35:46 +00:00
|
|
|
connect(m_buffer_size_box, qOverload<int>(&QSpinBox::valueChanged), this,
|
2018-10-09 22:52:19 +00:00
|
|
|
&NetPlayDialog::SaveSettings);
|
2022-09-11 00:35:20 +00:00
|
|
|
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);
|
2019-03-01 04:49:03 +00:00
|
|
|
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);
|
2019-04-02 12:08:27 +00:00
|
|
|
connect(m_golf_mode_action, &QAction::toggled, this, &NetPlayDialog::SaveSettings);
|
2019-04-02 21:13:42 +00:00
|
|
|
connect(m_golf_mode_overlay_action, &QAction::toggled, this, &NetPlayDialog::SaveSettings);
|
2019-05-30 21:58:31 +00:00
|
|
|
connect(m_fixed_delay_action, &QAction::toggled, this, &NetPlayDialog::SaveSettings);
|
2021-07-04 11:33:58 +00:00
|
|
|
connect(m_hide_remote_gbas_action, &QAction::toggled, this, &NetPlayDialog::SaveSettings);
|
2017-07-21 20:48:21 +00:00
|
|
|
}
|
|
|
|
|
2019-03-17 00:09:06 +00:00
|
|
|
void NetPlayDialog::SendMessage(const std::string& msg)
|
|
|
|
{
|
|
|
|
Settings::Instance().GetNetPlayClient()->SendChatMessage(msg);
|
|
|
|
|
2019-04-02 21:23:38 +00:00
|
|
|
DisplayMessage(
|
|
|
|
QStringLiteral("%1: %2").arg(QString::fromStdString(m_nickname), QString::fromStdString(msg)),
|
|
|
|
"");
|
2019-03-17 00:09:06 +00:00
|
|
|
}
|
|
|
|
|
2017-07-21 20:48:21 +00:00
|
|
|
void NetPlayDialog::OnChat()
|
|
|
|
{
|
|
|
|
QueueOnObject(this, [this] {
|
|
|
|
auto msg = m_chat_type_edit->text().toStdString();
|
2019-03-16 14:33:38 +00:00
|
|
|
|
|
|
|
if (msg.empty())
|
|
|
|
return;
|
|
|
|
|
2017-07-21 20:48:21 +00:00
|
|
|
m_chat_type_edit->clear();
|
|
|
|
|
2019-03-17 00:09:06 +00:00
|
|
|
SendMessage(msg);
|
2017-07-21 20:48:21 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-04-10 23:21:40 +00:00
|
|
|
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");
|
|
|
|
}
|
|
|
|
|
2017-07-21 20:48:21 +00:00
|
|
|
void NetPlayDialog::OnStart()
|
|
|
|
{
|
|
|
|
if (!Settings::Instance().GetNetPlayClient()->DoAllPlayersHaveGame())
|
|
|
|
{
|
2019-03-04 19:49:00 +00:00
|
|
|
if (ModalMessageBox::question(
|
|
|
|
this, tr("Warning"),
|
|
|
|
tr("Not all players have the game. Do you really want to start?")) == QMessageBox::No)
|
2017-07-21 20:48:21 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-03-01 04:49:03 +00:00
|
|
|
if (m_strict_settings_sync_action->isChecked() && Config::Get(Config::GFX_EFB_SCALE) == 0)
|
2018-07-19 22:10:37 +00:00
|
|
|
{
|
2019-03-04 19:49:00 +00:00
|
|
|
ModalMessageBox::critical(
|
2018-07-19 22:10:37 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2020-06-07 20:58:03 +00:00
|
|
|
const auto game = FindGameFile(m_current_game_identifier);
|
2018-07-19 22:10:37 +00:00
|
|
|
if (!game)
|
|
|
|
{
|
2020-11-26 02:13:50 +00:00
|
|
|
PanicAlertFmtT("Selected game doesn't exist in game list!");
|
2018-07-19 22:10:37 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-07-04 21:01:50 +00:00
|
|
|
if (Settings::Instance().GetNetPlayServer()->RequestStartGame())
|
|
|
|
SetOptionsEnabled(false);
|
2017-07-21 20:48:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void NetPlayDialog::reject()
|
|
|
|
{
|
2019-03-04 19:49:00 +00:00
|
|
|
if (ModalMessageBox::question(this, tr("Confirmation"),
|
|
|
|
tr("Are you sure you want to quit NetPlay?")) == QMessageBox::Yes)
|
2017-07-21 20:48:21 +00:00
|
|
|
{
|
|
|
|
QDialog::reject();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void NetPlayDialog::show(std::string nickname, bool use_traversal)
|
|
|
|
{
|
|
|
|
m_nickname = nickname;
|
|
|
|
m_use_traversal = use_traversal;
|
2018-05-10 17:26:42 +00:00
|
|
|
m_buffer_size = 0;
|
2018-07-03 21:50:08 +00:00
|
|
|
m_old_player_count = 0;
|
2017-07-21 20:48:21 +00:00
|
|
|
|
|
|
|
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"));
|
2018-11-15 05:58:07 +00:00
|
|
|
m_room_box->addItem(tr("External"));
|
2017-07-21 20:48:21 +00:00
|
|
|
|
|
|
|
for (const auto& iface : Settings::Instance().GetNetPlayServer()->GetInterfaceSet())
|
2018-05-11 19:22:57 +00:00
|
|
|
{
|
|
|
|
const auto interface = QString::fromStdString(iface);
|
|
|
|
m_room_box->addItem(iface == "!local!" ? tr("Local") : interface, interface);
|
|
|
|
}
|
2017-07-21 20:48:21 +00:00
|
|
|
}
|
|
|
|
|
2019-03-01 04:49:03 +00:00
|
|
|
m_data_menu->menuAction()->setVisible(is_hosting);
|
|
|
|
m_network_menu->menuAction()->setVisible(is_hosting);
|
2022-07-28 01:43:16 +00:00
|
|
|
m_game_digest_menu->menuAction()->setVisible(is_hosting);
|
2021-07-04 11:33:58 +00:00
|
|
|
#ifdef HAS_LIBMGBA
|
|
|
|
m_hide_remote_gbas_action->setVisible(is_hosting);
|
|
|
|
#else
|
|
|
|
m_hide_remote_gbas_action->setVisible(false);
|
|
|
|
#endif
|
2017-07-21 20:48:21 +00:00
|
|
|
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);
|
|
|
|
|
2019-03-28 06:32:06 +00:00
|
|
|
SetOptionsEnabled(true);
|
|
|
|
|
2017-07-21 20:48:21 +00:00
|
|
|
QDialog::show();
|
|
|
|
UpdateGUI();
|
|
|
|
}
|
|
|
|
|
2018-11-15 05:58:07 +00:00
|
|
|
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 "";
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-07-20 22:27:43 +00:00
|
|
|
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
|
2020-06-07 20:58:03 +00:00
|
|
|
if (m_player_count == 0 || m_current_game_name.empty())
|
2018-07-20 22:27:43 +00:00
|
|
|
return;
|
|
|
|
|
|
|
|
const auto use_default = [this]() {
|
2020-06-07 20:58:03 +00:00
|
|
|
Discord::UpdateDiscordPresence(m_player_count, Discord::SecretType::Empty, "",
|
|
|
|
m_current_game_name);
|
2018-07-20 22:27:43 +00:00
|
|
|
};
|
|
|
|
|
2018-08-06 21:56:40 +00:00
|
|
|
if (Core::IsRunning())
|
|
|
|
return use_default();
|
2018-07-20 22:27:43 +00:00
|
|
|
|
2018-08-06 21:56:40 +00:00
|
|
|
if (IsHosting())
|
2018-07-20 22:27:43 +00:00
|
|
|
{
|
2023-04-24 12:15:55 +00:00
|
|
|
if (Common::g_TraversalClient)
|
2018-07-20 22:27:43 +00:00
|
|
|
{
|
2023-04-24 12:15:55 +00:00
|
|
|
const auto host_id = Common::g_TraversalClient->GetHostID();
|
2018-08-06 21:56:40 +00:00
|
|
|
if (host_id[0] == '\0')
|
2018-07-20 22:27:43 +00:00
|
|
|
return use_default();
|
|
|
|
|
2018-08-06 21:56:40 +00:00
|
|
|
Discord::UpdateDiscordPresence(m_player_count, Discord::SecretType::RoomID,
|
2020-06-07 20:58:03 +00:00
|
|
|
std::string(host_id.begin(), host_id.end()),
|
|
|
|
m_current_game_name);
|
2018-08-06 21:56:40 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2018-11-15 05:58:07 +00:00
|
|
|
if (m_external_ip_address->empty())
|
|
|
|
return use_default();
|
2018-08-06 21:56:40 +00:00
|
|
|
const int port = Settings::Instance().GetNetPlayServer()->GetPort();
|
|
|
|
|
|
|
|
Discord::UpdateDiscordPresence(
|
|
|
|
m_player_count, Discord::SecretType::IPAddress,
|
2020-06-07 20:58:03 +00:00
|
|
|
Discord::CreateSecretFromIPAddress(*m_external_ip_address, port), m_current_game_name);
|
2018-08-06 21:56:40 +00:00
|
|
|
}
|
2018-07-20 22:27:43 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2018-08-06 21:56:40 +00:00
|
|
|
use_default();
|
2018-07-20 22:27:43 +00:00
|
|
|
}
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
2017-07-21 20:48:21 +00:00
|
|
|
void NetPlayDialog::UpdateGUI()
|
|
|
|
{
|
2018-07-13 00:37:12 +00:00
|
|
|
auto client = Settings::Instance().GetNetPlayClient();
|
|
|
|
auto server = Settings::Instance().GetNetPlayServer();
|
|
|
|
if (!client)
|
|
|
|
return;
|
2017-07-21 20:48:21 +00:00
|
|
|
|
2018-05-12 13:13:30 +00:00
|
|
|
// Update Player List
|
|
|
|
const auto players = client->GetPlayers();
|
2018-07-11 10:45:47 +00:00
|
|
|
|
|
|
|
if (static_cast<int>(players.size()) != m_player_count && m_player_count != 0)
|
|
|
|
QApplication::alert(this);
|
|
|
|
|
|
|
|
m_player_count = static_cast<int>(players.size());
|
|
|
|
|
2018-05-12 13:13:30 +00:00
|
|
|
int selection_pid = m_players_list->currentItem() ?
|
|
|
|
m_players_list->currentItem()->data(Qt::UserRole).toInt() :
|
|
|
|
-1;
|
2017-07-21 20:48:21 +00:00
|
|
|
|
|
|
|
m_players_list->clear();
|
2018-05-12 13:13:30 +00:00
|
|
|
m_players_list->setHorizontalHeaderLabels(
|
|
|
|
{tr("Player"), tr("Game Status"), tr("Ping"), tr("Mapping"), tr("Revision")});
|
2018-07-11 10:45:47 +00:00
|
|
|
m_players_list->setRowCount(m_player_count);
|
2018-05-12 13:13:30 +00:00
|
|
|
|
2022-05-21 05:25:33 +00:00
|
|
|
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")}},
|
|
|
|
};
|
2018-05-12 13:13:30 +00:00
|
|
|
|
2018-07-11 10:45:47 +00:00
|
|
|
for (int i = 0; i < m_player_count; i++)
|
2017-07-21 20:48:21 +00:00
|
|
|
{
|
2018-05-12 13:13:30 +00:00
|
|
|
const auto* p = players[i];
|
|
|
|
|
|
|
|
auto* name_item = new QTableWidgetItem(QString::fromStdString(p->name));
|
2022-05-21 05:25:33 +00:00
|
|
|
name_item->setToolTip(name_item->text());
|
|
|
|
const auto& status_info = player_status.count(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);
|
2018-05-12 13:13:30 +00:00
|
|
|
auto* ping_item = new QTableWidgetItem(QStringLiteral("%1 ms").arg(p->ping));
|
2022-05-21 05:25:33 +00:00
|
|
|
ping_item->setToolTip(ping_item->text());
|
2021-07-04 11:33:58 +00:00
|
|
|
auto* mapping_item =
|
|
|
|
new QTableWidgetItem(QString::fromStdString(NetPlay::GetPlayerMappingString(
|
|
|
|
p->pid, client->GetPadMapping(), client->GetGBAConfig(), client->GetWiimoteMapping())));
|
2022-05-21 05:25:33 +00:00
|
|
|
mapping_item->setToolTip(mapping_item->text());
|
2018-05-12 13:13:30 +00:00
|
|
|
auto* revision_item = new QTableWidgetItem(QString::fromStdString(p->revision));
|
2022-05-21 05:25:33 +00:00
|
|
|
revision_item->setToolTip(revision_item->text());
|
2018-05-12 13:13:30 +00:00
|
|
|
|
|
|
|
for (auto* item : {name_item, status_item, ping_item, mapping_item, revision_item})
|
2017-07-21 20:48:21 +00:00
|
|
|
{
|
2018-05-12 13:13:30 +00:00
|
|
|
item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
|
|
|
|
item->setData(Qt::UserRole, static_cast<int>(p->pid));
|
2017-07-21 20:48:21 +00:00
|
|
|
}
|
|
|
|
|
2018-05-12 13:13:30 +00:00
|
|
|
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);
|
|
|
|
}
|
2017-07-21 20:48:21 +00:00
|
|
|
|
2022-03-04 04:08:55 +00:00
|
|
|
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)
|
2017-07-21 20:48:21 +00:00
|
|
|
{
|
2023-04-24 12:15:55 +00:00
|
|
|
switch (Common::g_TraversalClient->GetState())
|
2017-07-21 20:48:21 +00:00
|
|
|
{
|
2023-04-24 12:15:55 +00:00
|
|
|
case Common::TraversalClient::State::Connecting:
|
2022-03-04 04:08:55 +00:00
|
|
|
m_hostcode_label->setText(tr("Connecting"));
|
2017-07-21 20:48:21 +00:00
|
|
|
m_hostcode_action_button->setEnabled(false);
|
2022-03-04 04:08:55 +00:00
|
|
|
m_hostcode_action_button->setText(tr("..."));
|
2017-07-21 20:48:21 +00:00
|
|
|
break;
|
2023-04-24 12:15:55 +00:00
|
|
|
case Common::TraversalClient::State::Connected:
|
2018-04-16 20:02:21 +00:00
|
|
|
{
|
2022-03-04 04:08:55 +00:00
|
|
|
if (m_room_box->currentIndex() == 0)
|
|
|
|
{
|
|
|
|
// Display Room ID.
|
2023-04-24 12:15:55 +00:00
|
|
|
const auto host_id = Common::g_TraversalClient->GetHostID();
|
2022-03-04 04:08:55 +00:00
|
|
|
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.
|
2023-04-24 12:15:55 +00:00
|
|
|
m_hostcode_label->setText(
|
|
|
|
InetAddressToString(Common::g_TraversalClient->GetExternalAddress()));
|
2022-03-04 04:08:55 +00:00
|
|
|
}
|
|
|
|
|
2017-07-21 20:48:21 +00:00
|
|
|
m_hostcode_action_button->setEnabled(true);
|
|
|
|
m_hostcode_action_button->setText(tr("Copy"));
|
|
|
|
m_is_copy_button_retry = false;
|
|
|
|
break;
|
2018-04-16 20:02:21 +00:00
|
|
|
}
|
2023-04-24 12:15:55 +00:00
|
|
|
case Common::TraversalClient::State::Failure:
|
2017-07-21 20:48:21 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
2022-03-04 04:08:55 +00:00
|
|
|
else
|
2017-07-21 20:48:21 +00:00
|
|
|
{
|
2022-03-04 04:08:55 +00:00
|
|
|
// Display External IP.
|
|
|
|
if (!m_external_ip_address->empty())
|
2018-11-15 05:58:07 +00:00
|
|
|
{
|
2022-03-04 04:08:55 +00:00
|
|
|
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);
|
2018-11-15 05:58:07 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2022-03-04 04:08:55 +00:00
|
|
|
m_hostcode_label->setText(tr("Unknown"));
|
|
|
|
m_hostcode_action_button->setEnabled(false);
|
2018-11-15 05:58:07 +00:00
|
|
|
}
|
|
|
|
|
2017-07-21 20:48:21 +00:00
|
|
|
m_hostcode_action_button->setText(tr("Copy"));
|
2018-11-15 05:58:07 +00:00
|
|
|
m_is_copy_button_retry = false;
|
2017-07-21 20:48:21 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// NetPlayUI methods
|
|
|
|
|
2021-11-20 18:59:14 +00:00
|
|
|
void NetPlayDialog::BootGame(const std::string& filename,
|
|
|
|
std::unique_ptr<BootSessionData> boot_session_data)
|
2017-07-21 20:48:21 +00:00
|
|
|
{
|
2018-07-03 23:02:13 +00:00
|
|
|
m_got_stop_request = false;
|
2021-11-20 18:59:14 +00:00
|
|
|
m_start_game_callback(filename, std::move(boot_session_data));
|
2017-07-21 20:48:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void NetPlayDialog::StopGame()
|
|
|
|
{
|
2018-07-03 23:02:13 +00:00
|
|
|
if (m_got_stop_request)
|
|
|
|
return;
|
|
|
|
|
|
|
|
m_got_stop_request = true;
|
2017-07-21 20:48:21 +00:00
|
|
|
emit Stop();
|
|
|
|
}
|
|
|
|
|
2018-07-04 21:01:50 +00:00
|
|
|
bool NetPlayDialog::IsHosting() const
|
|
|
|
{
|
|
|
|
return Settings::Instance().GetNetPlayServer() != nullptr;
|
|
|
|
}
|
|
|
|
|
2017-07-21 20:48:21 +00:00
|
|
|
void NetPlayDialog::Update()
|
|
|
|
{
|
2017-09-15 15:51:08 +00:00
|
|
|
QueueOnObject(this, &NetPlayDialog::UpdateGUI);
|
2017-07-21 20:48:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void NetPlayDialog::DisplayMessage(const QString& msg, const std::string& color, int duration)
|
|
|
|
{
|
|
|
|
QueueOnObject(m_chat_edit, [this, color, msg] {
|
2019-04-02 21:23:38 +00:00
|
|
|
m_chat_edit->append(QStringLiteral("<font color='%1'>%2</font>")
|
|
|
|
.arg(QString::fromStdString(color), msg.toHtmlEscaped()));
|
2017-07-21 20:48:21 +00:00
|
|
|
});
|
|
|
|
|
2019-03-17 00:09:06 +00:00
|
|
|
QColor c(color.empty() ? QStringLiteral("white") : QString::fromStdString(color));
|
2017-07-21 20:48:21 +00:00
|
|
|
|
2019-03-17 00:09:06 +00:00
|
|
|
if (g_ActiveConfig.bShowNetPlayMessages && Core::IsRunning())
|
|
|
|
g_netplay_chat_ui->AppendChat(msg.toStdString(),
|
|
|
|
{static_cast<float>(c.redF()), static_cast<float>(c.greenF()),
|
|
|
|
static_cast<float>(c.blueF())});
|
2017-07-21 20:48:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void NetPlayDialog::AppendChat(const std::string& msg)
|
|
|
|
{
|
2019-04-02 21:23:38 +00:00
|
|
|
DisplayMessage(QString::fromStdString(msg), "");
|
2018-07-11 10:45:47 +00:00
|
|
|
QApplication::alert(this);
|
2017-07-21 20:48:21 +00:00
|
|
|
}
|
|
|
|
|
2020-06-07 20:58:03 +00:00
|
|
|
void NetPlayDialog::OnMsgChangeGame(const NetPlay::SyncIdentifier& sync_identifier,
|
|
|
|
const std::string& netplay_name)
|
2017-07-21 20:48:21 +00:00
|
|
|
{
|
2020-06-07 20:58:03 +00:00
|
|
|
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;
|
2018-07-20 22:27:43 +00:00
|
|
|
UpdateDiscordPresence();
|
2017-07-21 20:48:21 +00:00
|
|
|
});
|
2020-06-07 20:58:03 +00:00
|
|
|
DisplayMessage(tr("Game changed to \"%1\"").arg(qname), "magenta");
|
2017-07-21 20:48:21 +00:00
|
|
|
}
|
|
|
|
|
2021-07-04 11:33:58 +00:00
|
|
|
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");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-07-21 20:48:21 +00:00
|
|
|
void NetPlayDialog::GameStatusChanged(bool running)
|
|
|
|
{
|
2018-07-04 21:01:50 +00:00
|
|
|
QueueOnObject(this, [this, running] { SetOptionsEnabled(!running); });
|
|
|
|
}
|
2017-07-21 20:48:21 +00:00
|
|
|
|
2018-07-04 21:01:50 +00:00
|
|
|
void NetPlayDialog::SetOptionsEnabled(bool enabled)
|
|
|
|
{
|
2018-07-13 00:37:12 +00:00
|
|
|
if (Settings::Instance().GetNetPlayServer())
|
2018-07-04 21:01:50 +00:00
|
|
|
{
|
|
|
|
m_start_button->setEnabled(enabled);
|
|
|
|
m_game_button->setEnabled(enabled);
|
2022-09-11 00:35:20 +00:00
|
|
|
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);
|
2019-03-01 04:49:03 +00:00
|
|
|
m_sync_codes_action->setEnabled(enabled);
|
2018-07-04 21:01:50 +00:00
|
|
|
m_assign_ports_button->setEnabled(enabled);
|
2019-03-01 04:49:03 +00:00
|
|
|
m_strict_settings_sync_action->setEnabled(enabled);
|
|
|
|
m_host_input_authority_action->setEnabled(enabled);
|
2019-05-30 21:58:31 +00:00
|
|
|
m_golf_mode_action->setEnabled(enabled);
|
|
|
|
m_fixed_delay_action->setEnabled(enabled);
|
2018-07-04 21:01:50 +00:00
|
|
|
}
|
|
|
|
|
2019-03-01 04:49:03 +00:00
|
|
|
m_record_input_action->setEnabled(enabled);
|
2017-07-21 20:48:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void NetPlayDialog::OnMsgStartGame()
|
|
|
|
{
|
|
|
|
DisplayMessage(tr("Started game"), "green");
|
|
|
|
|
2019-03-17 00:09:06 +00:00
|
|
|
g_netplay_chat_ui =
|
|
|
|
std::make_unique<NetPlayChatUI>([this](const std::string& message) { SendMessage(message); });
|
|
|
|
|
2022-09-18 23:18:26 +00:00
|
|
|
if (m_host_input_authority && Settings::Instance().GetNetPlayClient()->GetNetSettings().golf_mode)
|
2019-04-02 21:13:42 +00:00
|
|
|
{
|
|
|
|
g_netplay_golf_ui = std::make_unique<NetPlayGolfUI>(Settings::Instance().GetNetPlayClient());
|
|
|
|
}
|
|
|
|
|
2017-07-21 20:48:21 +00:00
|
|
|
QueueOnObject(this, [this] {
|
2018-07-13 00:37:12 +00:00
|
|
|
auto client = Settings::Instance().GetNetPlayClient();
|
2019-03-17 00:09:06 +00:00
|
|
|
|
2018-07-13 00:37:12 +00:00
|
|
|
if (client)
|
2020-06-07 20:58:03 +00:00
|
|
|
{
|
|
|
|
if (auto game = FindGameFile(m_current_game_identifier))
|
|
|
|
client->StartGame(game->GetFilePath());
|
|
|
|
else
|
2020-11-26 02:13:50 +00:00
|
|
|
PanicAlertFmtT("Selected game doesn't exist in game list!");
|
2020-06-07 20:58:03 +00:00
|
|
|
}
|
2018-08-06 21:56:40 +00:00
|
|
|
UpdateDiscordPresence();
|
2017-07-21 20:48:21 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
void NetPlayDialog::OnMsgStopGame()
|
|
|
|
{
|
2019-03-17 00:09:06 +00:00
|
|
|
g_netplay_chat_ui.reset();
|
2019-04-02 21:13:42 +00:00
|
|
|
g_netplay_golf_ui.reset();
|
2018-08-06 21:56:40 +00:00
|
|
|
QueueOnObject(this, [this] { UpdateDiscordPresence(); });
|
2017-07-21 20:48:21 +00:00
|
|
|
}
|
|
|
|
|
2018-11-11 03:37:49 +00:00
|
|
|
void NetPlayDialog::OnMsgPowerButton()
|
|
|
|
{
|
|
|
|
if (!Core::IsRunning())
|
|
|
|
return;
|
|
|
|
QueueOnObject(this, [] { UICommon::TriggerSTMPowerEvent(); });
|
|
|
|
}
|
|
|
|
|
2019-07-31 03:14:51 +00:00
|
|
|
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");
|
|
|
|
}
|
|
|
|
|
2017-07-21 20:48:21 +00:00
|
|
|
void NetPlayDialog::OnPadBufferChanged(u32 buffer)
|
|
|
|
{
|
NetPlay host input authority mode
Currently, each player buffers their own inputs and sends them to the
host. The host then relays those inputs to everyone else. Every player
waits on inputs from all players to be buffered before continuing. What
this means is all clients run in lockstep, and the total latency of
inputs cannot be lower than the sum of the 2 highest client ping times
in the game (in 3+ player sessions with people across the world, the
latency can be very high).
Host input authority mode changes it so players no longer buffer their
own inputs, and only send them to the host. The host stores only the
most recent input received from a player. The host then sends inputs
for all pads at the SI poll interval, similar to the existing code. If
a player sends inputs to slowly, their last received input is simply
sent again. If they send too quickly, inputs are dropped. This means
that the host has full control over what inputs are actually read by
the game, hence the name of the mode. Also, because the rate at which
inputs are received by SI is decoupled from the rate at which players
are sending inputs, clients are no longer dependent on each other. They
only care what the host is doing. This means that they can set their
buffer individually based on their latency to the host, rather than the
highest latency between any 2 players, allowing someone with lower ping
to the host to have less latency than someone else.
This is a catch to this: as a necessity of how the host's input sending
works, the host has 0 latency. There isn't a good way to fix this, as
input delay is now solely dependent on the real latency to the host's
server. Having differing latency between players would be considered
unfair for competitive play, but for casual play we don't really care.
For this reason though, combined with the potential for a few inputs to
be dropped on a bad connection, the old mode will remain and this new
mode is entirely optional.
2018-08-24 08:17:18 +00:00
|
|
|
QueueOnObject(this, [this, buffer] {
|
|
|
|
const QSignalBlocker blocker(m_buffer_size_box);
|
|
|
|
m_buffer_size_box->setValue(buffer);
|
|
|
|
});
|
2019-04-02 02:36:48 +00:00
|
|
|
DisplayMessage(m_host_input_authority ? tr("Max buffer size changed to %1").arg(buffer) :
|
|
|
|
tr("Buffer size changed to %1").arg(buffer),
|
2019-03-25 09:00:38 +00:00
|
|
|
"darkcyan");
|
2018-05-10 17:26:42 +00:00
|
|
|
|
|
|
|
m_buffer_size = static_cast<int>(buffer);
|
2017-07-21 20:48:21 +00:00
|
|
|
}
|
|
|
|
|
NetPlay host input authority mode
Currently, each player buffers their own inputs and sends them to the
host. The host then relays those inputs to everyone else. Every player
waits on inputs from all players to be buffered before continuing. What
this means is all clients run in lockstep, and the total latency of
inputs cannot be lower than the sum of the 2 highest client ping times
in the game (in 3+ player sessions with people across the world, the
latency can be very high).
Host input authority mode changes it so players no longer buffer their
own inputs, and only send them to the host. The host stores only the
most recent input received from a player. The host then sends inputs
for all pads at the SI poll interval, similar to the existing code. If
a player sends inputs to slowly, their last received input is simply
sent again. If they send too quickly, inputs are dropped. This means
that the host has full control over what inputs are actually read by
the game, hence the name of the mode. Also, because the rate at which
inputs are received by SI is decoupled from the rate at which players
are sending inputs, clients are no longer dependent on each other. They
only care what the host is doing. This means that they can set their
buffer individually based on their latency to the host, rather than the
highest latency between any 2 players, allowing someone with lower ping
to the host to have less latency than someone else.
This is a catch to this: as a necessity of how the host's input sending
works, the host has 0 latency. There isn't a good way to fix this, as
input delay is now solely dependent on the real latency to the host's
server. Having differing latency between players would be considered
unfair for competitive play, but for casual play we don't really care.
For this reason though, combined with the potential for a few inputs to
be dropped on a bad connection, the old mode will remain and this new
mode is entirely optional.
2018-08-24 08:17:18 +00:00
|
|
|
void NetPlayDialog::OnHostInputAuthorityChanged(bool enabled)
|
|
|
|
{
|
2019-04-02 02:36:48 +00:00
|
|
|
m_host_input_authority = enabled;
|
|
|
|
DisplayMessage(enabled ? tr("Host input authority enabled") : tr("Host input authority disabled"),
|
|
|
|
"");
|
|
|
|
|
NetPlay host input authority mode
Currently, each player buffers their own inputs and sends them to the
host. The host then relays those inputs to everyone else. Every player
waits on inputs from all players to be buffered before continuing. What
this means is all clients run in lockstep, and the total latency of
inputs cannot be lower than the sum of the 2 highest client ping times
in the game (in 3+ player sessions with people across the world, the
latency can be very high).
Host input authority mode changes it so players no longer buffer their
own inputs, and only send them to the host. The host stores only the
most recent input received from a player. The host then sends inputs
for all pads at the SI poll interval, similar to the existing code. If
a player sends inputs to slowly, their last received input is simply
sent again. If they send too quickly, inputs are dropped. This means
that the host has full control over what inputs are actually read by
the game, hence the name of the mode. Also, because the rate at which
inputs are received by SI is decoupled from the rate at which players
are sending inputs, clients are no longer dependent on each other. They
only care what the host is doing. This means that they can set their
buffer individually based on their latency to the host, rather than the
highest latency between any 2 players, allowing someone with lower ping
to the host to have less latency than someone else.
This is a catch to this: as a necessity of how the host's input sending
works, the host has 0 latency. There isn't a good way to fix this, as
input delay is now solely dependent on the real latency to the host's
server. Having differing latency between players would be considered
unfair for competitive play, but for casual play we don't really care.
For this reason though, combined with the potential for a few inputs to
be dropped on a bad connection, the old mode will remain and this new
mode is entirely optional.
2018-08-24 08:17:18 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2019-04-02 02:36:48 +00:00
|
|
|
m_buffer_label->setText(enabled ? tr("Max Buffer:") : tr("Buffer:"));
|
|
|
|
if (enabled)
|
2019-05-30 21:58:31 +00:00
|
|
|
{
|
|
|
|
const QSignalBlocker blocker(m_buffer_size_box);
|
2019-04-02 02:36:48 +00:00
|
|
|
m_buffer_size_box->setValue(Config::Get(Config::NETPLAY_CLIENT_BUFFER_SIZE));
|
2019-05-30 21:58:31 +00:00
|
|
|
}
|
2019-04-02 02:36:48 +00:00
|
|
|
});
|
NetPlay host input authority mode
Currently, each player buffers their own inputs and sends them to the
host. The host then relays those inputs to everyone else. Every player
waits on inputs from all players to be buffered before continuing. What
this means is all clients run in lockstep, and the total latency of
inputs cannot be lower than the sum of the 2 highest client ping times
in the game (in 3+ player sessions with people across the world, the
latency can be very high).
Host input authority mode changes it so players no longer buffer their
own inputs, and only send them to the host. The host stores only the
most recent input received from a player. The host then sends inputs
for all pads at the SI poll interval, similar to the existing code. If
a player sends inputs to slowly, their last received input is simply
sent again. If they send too quickly, inputs are dropped. This means
that the host has full control over what inputs are actually read by
the game, hence the name of the mode. Also, because the rate at which
inputs are received by SI is decoupled from the rate at which players
are sending inputs, clients are no longer dependent on each other. They
only care what the host is doing. This means that they can set their
buffer individually based on their latency to the host, rather than the
highest latency between any 2 players, allowing someone with lower ping
to the host to have less latency than someone else.
This is a catch to this: as a necessity of how the host's input sending
works, the host has 0 latency. There isn't a good way to fix this, as
input delay is now solely dependent on the real latency to the host's
server. Having differing latency between players would be considered
unfair for competitive play, but for casual play we don't really care.
For this reason though, combined with the potential for a few inputs to
be dropped on a bad connection, the old mode will remain and this new
mode is entirely optional.
2018-08-24 08:17:18 +00:00
|
|
|
}
|
|
|
|
|
2017-07-21 20:48:21 +00:00
|
|
|
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");
|
|
|
|
}
|
|
|
|
|
2018-07-02 02:52:43 +00:00
|
|
|
void NetPlayDialog::OnConnectionError(const std::string& message)
|
|
|
|
{
|
|
|
|
QueueOnObject(this, [this, message] {
|
2019-03-04 19:49:00 +00:00
|
|
|
ModalMessageBox::critical(this, tr("Error"),
|
|
|
|
tr("Failed to connect to server: %1").arg(tr(message.c_str())));
|
2018-07-02 02:52:43 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-04-24 12:15:55 +00:00
|
|
|
void NetPlayDialog::OnTraversalError(Common::TraversalClient::FailureReason error)
|
2017-07-21 20:48:21 +00:00
|
|
|
{
|
|
|
|
QueueOnObject(this, [this, error] {
|
|
|
|
switch (error)
|
|
|
|
{
|
2023-04-24 12:15:55 +00:00
|
|
|
case Common::TraversalClient::FailureReason::BadHost:
|
2019-03-04 19:49:00 +00:00
|
|
|
ModalMessageBox::critical(this, tr("Traversal Error"), tr("Couldn't look up central server"));
|
2017-07-21 20:48:21 +00:00
|
|
|
QDialog::reject();
|
|
|
|
break;
|
2023-04-24 12:15:55 +00:00
|
|
|
case Common::TraversalClient::FailureReason::VersionTooOld:
|
2019-03-04 19:49:00 +00:00
|
|
|
ModalMessageBox::critical(this, tr("Traversal Error"),
|
|
|
|
tr("Dolphin is too old for traversal server"));
|
2017-07-21 20:48:21 +00:00
|
|
|
QDialog::reject();
|
|
|
|
break;
|
2023-04-24 12:15:55 +00:00
|
|
|
case Common::TraversalClient::FailureReason::ServerForgotAboutUs:
|
|
|
|
case Common::TraversalClient::FailureReason::SocketSendError:
|
|
|
|
case Common::TraversalClient::FailureReason::ResendTimeout:
|
2017-07-21 20:48:21 +00:00
|
|
|
UpdateGUI();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-04-24 12:15:55 +00:00
|
|
|
void NetPlayDialog::OnTraversalStateChanged(Common::TraversalClient::State state)
|
2018-07-20 22:27:43 +00:00
|
|
|
{
|
|
|
|
switch (state)
|
|
|
|
{
|
2023-04-24 12:15:55 +00:00
|
|
|
case Common::TraversalClient::State::Connected:
|
|
|
|
case Common::TraversalClient::State::Failure:
|
2018-07-20 22:27:43 +00:00
|
|
|
UpdateDiscordPresence();
|
2021-01-19 19:00:01 +00:00
|
|
|
break;
|
2018-07-20 22:27:43 +00:00
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-28 06:32:06 +00:00
|
|
|
void NetPlayDialog::OnGameStartAborted()
|
2018-07-04 21:01:50 +00:00
|
|
|
{
|
|
|
|
QueueOnObject(this, [this] { SetOptionsEnabled(true); });
|
|
|
|
}
|
|
|
|
|
2019-04-02 12:08:27 +00:00
|
|
|
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)), "");
|
|
|
|
}
|
|
|
|
|
2017-07-21 20:48:21 +00:00
|
|
|
bool NetPlayDialog::IsRecording()
|
|
|
|
{
|
2019-03-01 04:49:03 +00:00
|
|
|
std::optional<bool> is_recording = RunOnObject(m_record_input_action, &QAction::isChecked);
|
2018-05-21 22:27:12 +00:00
|
|
|
if (is_recording)
|
|
|
|
return *is_recording;
|
|
|
|
return false;
|
2017-07-21 20:48:21 +00:00
|
|
|
}
|
|
|
|
|
2020-06-07 20:58:03 +00:00
|
|
|
std::shared_ptr<const UICommon::GameFile>
|
|
|
|
NetPlayDialog::FindGameFile(const NetPlay::SyncIdentifier& sync_identifier,
|
|
|
|
NetPlay::SyncIdentifierComparison* found)
|
2017-07-21 20:48:21 +00:00
|
|
|
{
|
2020-06-07 20:58:03 +00:00
|
|
|
NetPlay::SyncIdentifierComparison temp;
|
|
|
|
if (!found)
|
|
|
|
found = &temp;
|
|
|
|
|
|
|
|
*found = NetPlay::SyncIdentifierComparison::DifferentGame;
|
2017-07-21 20:48:21 +00:00
|
|
|
|
2018-07-04 21:01:50 +00:00
|
|
|
std::optional<std::shared_ptr<const UICommon::GameFile>> game_file =
|
2020-06-07 20:58:03 +00:00
|
|
|
RunOnObject(this, [this, &sync_identifier, found] {
|
2019-01-17 22:28:07 +00:00
|
|
|
for (int i = 0; i < m_game_list_model.rowCount(QModelIndex()); i++)
|
2018-07-04 21:01:50 +00:00
|
|
|
{
|
2020-11-21 00:58:22 +00:00
|
|
|
auto file = m_game_list_model.GetGameFile(i);
|
|
|
|
*found = std::min(*found, file->CompareSyncIdentifier(sync_identifier));
|
2020-06-07 20:58:03 +00:00
|
|
|
if (*found == NetPlay::SyncIdentifierComparison::SameGame)
|
2020-11-21 00:58:22 +00:00
|
|
|
return file;
|
2018-07-04 21:01:50 +00:00
|
|
|
}
|
|
|
|
return static_cast<std::shared_ptr<const UICommon::GameFile>>(nullptr);
|
|
|
|
});
|
|
|
|
if (game_file)
|
|
|
|
return *game_file;
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
2021-07-04 11:33:58 +00:00
|
|
|
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 {};
|
|
|
|
}
|
|
|
|
|
2019-05-30 21:44:02 +00:00
|
|
|
void NetPlayDialog::LoadSettings()
|
|
|
|
{
|
|
|
|
const int buffer_size = Config::Get(Config::NETPLAY_BUFFER_SIZE);
|
2022-09-11 00:35:20 +00:00
|
|
|
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);
|
2019-05-30 21:44:02 +00:00
|
|
|
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);
|
2021-07-04 11:33:58 +00:00
|
|
|
const bool hide_remote_gbas = Config::Get(Config::NETPLAY_HIDE_REMOTE_GBAS);
|
2019-05-30 21:44:02 +00:00
|
|
|
|
|
|
|
m_buffer_size_box->setValue(buffer_size);
|
2022-09-11 00:35:20 +00:00
|
|
|
|
|
|
|
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);
|
|
|
|
|
2019-05-30 21:44:02 +00:00
|
|
|
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);
|
2021-07-04 11:33:58 +00:00
|
|
|
m_hide_remote_gbas_action->setChecked(hide_remote_gbas);
|
2019-05-30 21:58:31 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
{
|
2020-11-26 02:13:50 +00:00
|
|
|
WARN_LOG_FMT(NETPLAY, "Unknown network mode '{}', using 'fixeddelay'", network_mode);
|
2019-05-30 21:58:31 +00:00
|
|
|
m_fixed_delay_action->setChecked(true);
|
|
|
|
}
|
2019-05-30 21:44:02 +00:00
|
|
|
}
|
|
|
|
|
2018-10-09 22:52:19 +00:00
|
|
|
void NetPlayDialog::SaveSettings()
|
|
|
|
{
|
2019-03-03 16:58:37 +00:00
|
|
|
Config::ConfigChangeCallbackGuard config_guard;
|
|
|
|
|
2018-10-09 22:52:19 +00:00
|
|
|
if (m_host_input_authority)
|
2019-04-02 02:36:48 +00:00
|
|
|
Config::SetBase(Config::NETPLAY_CLIENT_BUFFER_SIZE, m_buffer_size_box->value());
|
2018-10-09 22:52:19 +00:00
|
|
|
else
|
|
|
|
Config::SetBase(Config::NETPLAY_BUFFER_SIZE, m_buffer_size_box->value());
|
2019-04-02 02:36:48 +00:00
|
|
|
|
2022-09-11 00:35:20 +00:00
|
|
|
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());
|
2019-03-01 04:49:03 +00:00
|
|
|
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());
|
2019-04-02 21:13:42 +00:00
|
|
|
Config::SetBase(Config::NETPLAY_GOLF_MODE_OVERLAY, m_golf_mode_overlay_action->isChecked());
|
2021-07-04 11:33:58 +00:00
|
|
|
Config::SetBase(Config::NETPLAY_HIDE_REMOTE_GBAS, m_hide_remote_gbas_action->isChecked());
|
2019-05-30 21:58:31 +00:00
|
|
|
|
|
|
|
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);
|
2018-10-09 22:52:19 +00:00
|
|
|
}
|
|
|
|
|
2022-07-28 01:43:16 +00:00
|
|
|
void NetPlayDialog::ShowGameDigestDialog(const std::string& title)
|
2017-07-21 20:48:21 +00:00
|
|
|
{
|
2020-06-07 20:58:03 +00:00
|
|
|
QueueOnObject(this, [this, title] {
|
2022-07-28 01:43:16 +00:00
|
|
|
m_game_digest_menu->setEnabled(false);
|
2017-07-21 20:48:21 +00:00
|
|
|
|
2022-07-28 01:43:16 +00:00
|
|
|
if (m_game_digest_dialog->isVisible())
|
|
|
|
m_game_digest_dialog->close();
|
2017-07-21 20:48:21 +00:00
|
|
|
|
2022-07-28 01:43:16 +00:00
|
|
|
m_game_digest_dialog->show(QString::fromStdString(title));
|
2017-07-21 20:48:21 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-07-28 01:43:16 +00:00
|
|
|
void NetPlayDialog::SetGameDigestProgress(int pid, int progress)
|
2017-07-21 20:48:21 +00:00
|
|
|
{
|
|
|
|
QueueOnObject(this, [this, pid, progress] {
|
2022-07-28 01:43:16 +00:00
|
|
|
if (m_game_digest_dialog->isVisible())
|
|
|
|
m_game_digest_dialog->SetProgress(pid, progress);
|
2017-07-21 20:48:21 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-07-28 01:43:16 +00:00
|
|
|
void NetPlayDialog::SetGameDigestResult(int pid, const std::string& result)
|
2017-07-21 20:48:21 +00:00
|
|
|
{
|
|
|
|
QueueOnObject(this, [this, pid, result] {
|
2022-07-28 01:43:16 +00:00
|
|
|
m_game_digest_dialog->SetResult(pid, result);
|
|
|
|
m_game_digest_menu->setEnabled(true);
|
2017-07-21 20:48:21 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-07-28 01:43:16 +00:00
|
|
|
void NetPlayDialog::AbortGameDigest()
|
2017-07-21 20:48:21 +00:00
|
|
|
{
|
|
|
|
QueueOnObject(this, [this] {
|
2022-07-28 01:43:16 +00:00
|
|
|
m_game_digest_dialog->close();
|
|
|
|
m_game_digest_menu->setEnabled(true);
|
2017-07-21 20:48:21 +00:00
|
|
|
});
|
|
|
|
}
|
2018-10-18 08:33:05 +00:00
|
|
|
|
|
|
|
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())
|
2019-03-30 10:20:24 +00:00
|
|
|
m_chunked_progress_dialog->done(QDialog::Accepted);
|
2018-10-18 08:33:05 +00:00
|
|
|
|
|
|
|
m_chunked_progress_dialog->show(QString::fromStdString(title), data_size, players);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
void NetPlayDialog::HideChunkedProgressDialog()
|
|
|
|
{
|
2019-03-30 10:20:24 +00:00
|
|
|
QueueOnObject(this, [this] { m_chunked_progress_dialog->done(QDialog::Accepted); });
|
2018-10-18 08:33:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
});
|
|
|
|
}
|
2021-11-20 20:03:34 +00:00
|
|
|
|
2021-11-14 01:22:59 +00:00
|
|
|
void NetPlayDialog::SetHostWiiSyncData(std::vector<u64> titles, std::string redirect_folder)
|
2021-11-20 20:03:34 +00:00
|
|
|
{
|
|
|
|
auto client = Settings::Instance().GetNetPlayClient();
|
|
|
|
if (client)
|
2021-11-14 01:22:59 +00:00
|
|
|
client->SetWiiSyncData(nullptr, std::move(titles), std::move(redirect_folder));
|
2021-11-20 20:03:34 +00:00
|
|
|
}
|