// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team // SPDX-License-Identifier: GPL-3.0+ #include #include #include #include "common/FileSystem.h" #include "common/Path.h" #include "common/StringUtil.h" #include "pcsx2/Host.h" #include "pcsx2/INISettingsInterface.h" #include "DEV9SettingsWidget.h" #include "QtHost.h" #include "QtUtils.h" #include "SettingWidgetBinder.h" #include "SettingsWindow.h" #include "HddCreateQt.h" #include "DEV9/pcap_io.h" #ifdef _WIN32 #include "DEV9/Win32/tap.h" #endif #include "DEV9/sockets.h" static const char* s_api_name[] = { " ", QT_TRANSLATE_NOOP("DEV9SettingsWidget", "PCAP Bridged"), QT_TRANSLATE_NOOP("DEV9SettingsWidget", "PCAP Switched"), QT_TRANSLATE_NOOP("DEV9SettingsWidget", "TAP"), QT_TRANSLATE_NOOP("DEV9SettingsWidget", "Sockets"), nullptr, }; static const char* s_dns_name[] = { QT_TRANSLATE_NOOP("DEV9SettingsWidget", "Manual"), QT_TRANSLATE_NOOP("DEV9SettingsWidget", "Auto"), QT_TRANSLATE_NOOP("DEV9SettingsWidget", "Internal"), nullptr, }; using PacketReader::IP::IP_Address; DEV9SettingsWidget::DEV9SettingsWidget(SettingsWindow* dialog, QWidget* parent) : QWidget(parent) , m_dialog{dialog} { SettingsInterface* sif = dialog->getSettingsInterface(); m_ui.setupUi(this); ////////////////////////////////////////////////////////////////////////// // Eth Enabled ////////////////////////////////////////////////////////////////////////// //Connect needs to be after BindWidgetToBoolSetting to ensure correct order of execution for disabling a per game setting //we then need to manually call onEthAutoChanged to update the UI on fist load (done in show) SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.ethEnabled, "DEV9/Eth", "EthEnable", false); connect(m_ui.ethEnabled, &QCheckBox::checkStateChanged, this, &DEV9SettingsWidget::onEthEnabledChanged); ////////////////////////////////////////////////////////////////////////// // Eth Device Settings ////////////////////////////////////////////////////////////////////////// connect(m_ui.ethDevType, QOverload::of(&QComboBox::currentIndexChanged), this, &DEV9SettingsWidget::onEthDeviceTypeChanged); connect(m_ui.ethDev, QOverload::of(&QComboBox::currentIndexChanged), this, &DEV9SettingsWidget::onEthDeviceChanged); //Comboboxes populated in show event ////////////////////////////////////////////////////////////////////////// // DHCP Settings ////////////////////////////////////////////////////////////////////////// SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.ethInterceptDHCP, "DEV9/Eth", "InterceptDHCP", false); onEthDHCPInterceptChanged(m_ui.ethInterceptDHCP->checkState()); connect(m_ui.ethInterceptDHCP, &QCheckBox::checkStateChanged, this, &DEV9SettingsWidget::onEthDHCPInterceptChanged); //IP settings const IPValidator* ipValidator = new IPValidator(this, m_dialog->isPerGameSettings()); // clang-format off m_ui.ethPS2Addr ->setValidator(ipValidator); m_ui.ethNetMask ->setValidator(ipValidator); m_ui.ethGatewayAddr->setValidator(ipValidator); m_ui.ethDNS1Addr ->setValidator(ipValidator); m_ui.ethDNS2Addr ->setValidator(ipValidator); if (m_dialog->isPerGameSettings()) { m_ui.ethPS2Addr ->setText(QString::fromUtf8(m_dialog->getStringValue("DEV9/Eth", "PS2IP", "").value().c_str())); m_ui.ethNetMask ->setText(QString::fromUtf8(m_dialog->getStringValue("DEV9/Eth", "Mask", "").value().c_str())); m_ui.ethGatewayAddr->setText(QString::fromUtf8(m_dialog->getStringValue("DEV9/Eth", "Gateway", "").value().c_str())); m_ui.ethDNS1Addr ->setText(QString::fromUtf8(m_dialog->getStringValue("DEV9/Eth", "DNS1", "").value().c_str())); m_ui.ethDNS2Addr ->setText(QString::fromUtf8(m_dialog->getStringValue("DEV9/Eth", "DNS2", "").value().c_str())); m_ui.ethPS2Addr ->setPlaceholderText(QString::fromUtf8(Host::GetBaseStringSettingValue("DEV9/Eth", "PS2IP", "0.0.0.0").c_str())); m_ui.ethNetMask ->setPlaceholderText(QString::fromUtf8(Host::GetBaseStringSettingValue("DEV9/Eth", "Mask", "0.0.0.0").c_str())); m_ui.ethGatewayAddr->setPlaceholderText(QString::fromUtf8(Host::GetBaseStringSettingValue("DEV9/Eth", "Gateway", "0.0.0.0").c_str())); m_ui.ethDNS1Addr ->setPlaceholderText(QString::fromUtf8(Host::GetBaseStringSettingValue("DEV9/Eth", "DNS1", "0.0.0.0").c_str())); m_ui.ethDNS2Addr ->setPlaceholderText(QString::fromUtf8(Host::GetBaseStringSettingValue("DEV9/Eth", "DNS2", "0.0.0.0").c_str())); } else { m_ui.ethPS2Addr ->setText(QString::fromUtf8(m_dialog->getStringValue("DEV9/Eth", "PS2IP", "0.0.0.0").value().c_str())); m_ui.ethNetMask ->setText(QString::fromUtf8(m_dialog->getStringValue("DEV9/Eth", "Mask", "0.0.0.0").value().c_str())); m_ui.ethGatewayAddr->setText(QString::fromUtf8(m_dialog->getStringValue("DEV9/Eth", "Gateway", "0.0.0.0").value().c_str())); m_ui.ethDNS1Addr ->setText(QString::fromUtf8(m_dialog->getStringValue("DEV9/Eth", "DNS1", "0.0.0.0").value().c_str())); m_ui.ethDNS2Addr ->setText(QString::fromUtf8(m_dialog->getStringValue("DEV9/Eth", "DNS2", "0.0.0.0").value().c_str())); } connect(m_ui.ethPS2Addr, &QLineEdit::editingFinished, this, [&]() { onEthIPChanged(m_ui.ethPS2Addr, "DEV9/Eth", "PS2IP" ); }); connect(m_ui.ethNetMask, &QLineEdit::editingFinished, this, [&]() { onEthIPChanged(m_ui.ethNetMask, "DEV9/Eth", "Mask" ); }); connect(m_ui.ethGatewayAddr, &QLineEdit::editingFinished, this, [&]() { onEthIPChanged(m_ui.ethGatewayAddr, "DEV9/Eth", "Gateway"); }); connect(m_ui.ethDNS1Addr, &QLineEdit::editingFinished, this, [&]() { onEthIPChanged(m_ui.ethDNS1Addr, "DEV9/Eth", "DNS1" ); }); connect(m_ui.ethDNS2Addr, &QLineEdit::editingFinished, this, [&]() { onEthIPChanged(m_ui.ethDNS2Addr, "DEV9/Eth", "DNS2" ); }); // clang-format on //Auto SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.ethNetMaskAuto, "DEV9/Eth", "AutoMask", true); onEthAutoChanged(m_ui.ethNetMaskAuto, m_ui.ethNetMaskAuto->checkState(), m_ui.ethNetMask, "DEV9/Eth", "AutoMask"); connect(m_ui.ethNetMaskAuto, &QCheckBox::checkStateChanged, this, [&](Qt::CheckState state) { onEthAutoChanged(m_ui.ethNetMaskAuto, state, m_ui.ethNetMask, "DEV9/Eth", "AutoMask"); }); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.ethGatewayAuto, "DEV9/Eth", "AutoGateway", true); onEthAutoChanged(m_ui.ethGatewayAuto, m_ui.ethGatewayAuto->checkState(), m_ui.ethGatewayAddr, "DEV9/Eth", "AutoGateway"); connect(m_ui.ethGatewayAuto, &QCheckBox::checkStateChanged, this, [&](Qt::CheckState state) { onEthAutoChanged(m_ui.ethGatewayAuto, state, m_ui.ethGatewayAddr, "DEV9/Eth", "AutoGateway"); }); SettingWidgetBinder::BindWidgetToEnumSetting(sif, m_ui.ethDNS1Mode, "DEV9/Eth", "ModeDNS1", s_dns_name, Pcsx2Config::DEV9Options::DnsModeNames, Pcsx2Config::DEV9Options::DnsModeNames[static_cast(Pcsx2Config::DEV9Options::DnsMode::Auto)], "DEV9SettingsWidget"); onEthDNSModeChanged(m_ui.ethDNS1Mode, m_ui.ethDNS1Mode->currentIndex(), m_ui.ethDNS1Addr, "DEV9/Eth", "ModeDNS1"); connect(m_ui.ethDNS1Mode, QOverload::of(&QComboBox::currentIndexChanged), this, [&](int index) { onEthDNSModeChanged(m_ui.ethDNS1Mode, index, m_ui.ethDNS1Addr, "DEV9/Eth", "ModeDNS1"); }); SettingWidgetBinder::BindWidgetToEnumSetting(sif, m_ui.ethDNS2Mode, "DEV9/Eth", "ModeDNS2", s_dns_name, Pcsx2Config::DEV9Options::DnsModeNames, Pcsx2Config::DEV9Options::DnsModeNames[static_cast(Pcsx2Config::DEV9Options::DnsMode::Auto)], "DEV9SettingsWidget"); onEthDNSModeChanged(m_ui.ethDNS2Mode, m_ui.ethDNS2Mode->currentIndex(), m_ui.ethDNS2Addr, "DEV9/Eth", "ModeDNS2"); connect(m_ui.ethDNS2Mode, QOverload::of(&QComboBox::currentIndexChanged), this, [&](int index) { onEthDNSModeChanged(m_ui.ethDNS2Mode, index, m_ui.ethDNS2Addr, "DEV9/Eth", "ModeDNS2"); }); ////////////////////////////////////////////////////////////////////////// // DNS Settings ////////////////////////////////////////////////////////////////////////// m_ethHost_model = new QStandardItemModel(0, 4, m_ui.ethHosts); QStringList headers; headers.push_back(tr("Name")); headers.push_back(tr("Url")); headers.push_back(tr("Address")); headers.push_back(tr("Enabled")); m_ethHost_model->setHorizontalHeaderLabels(headers); connect(m_ethHost_model, QOverload::of(&QStandardItemModel::itemChanged), this, &DEV9SettingsWidget::onEthHostEdit); m_ethHosts_proxy = new QSortFilterProxyModel(m_ui.ethHosts); m_ethHosts_proxy->setSourceModel(m_ethHost_model); m_ui.ethHosts->setModel(m_ethHosts_proxy); m_ui.ethHosts->setItemDelegateForColumn(2, new IPItemDelegate(m_ui.ethHosts)); RefreshHostList(); m_ui.ethHosts->installEventFilter(this); connect(m_ui.ethHostAdd, &QPushButton::clicked, this, &DEV9SettingsWidget::onEthHostAdd); connect(m_ui.ethHostDel, &QPushButton::clicked, this, &DEV9SettingsWidget::onEthHostDel); connect(m_ui.ethHostExport, &QPushButton::clicked, this, &DEV9SettingsWidget::onEthHostExport); connect(m_ui.ethHostImport, &QPushButton::clicked, this, &DEV9SettingsWidget::onEthHostImport); connect(m_ui.ethHostPerGame, &QPushButton::clicked, this, &DEV9SettingsWidget::onEthHostPerGame); ////////////////////////////////////////////////////////////////////////// // HDD Settings ////////////////////////////////////////////////////////////////////////// connect(m_ui.hddEnabled, &QCheckBox::checkStateChanged, this, &DEV9SettingsWidget::onHddEnabledChanged); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.hddEnabled, "DEV9/Hdd", "HddEnable", false); if (m_dialog->isPerGameSettings()) { m_ui.hddFile->setText(QString::fromUtf8(m_dialog->getStringValue("DEV9/Hdd", "HddFile", "").value().c_str())); m_ui.hddFile->setPlaceholderText(QString::fromUtf8(Host::GetBaseStringSettingValue("DEV9/Hdd", "HddFile", "DEV9hdd.raw"))); } else m_ui.hddFile->setText(QString::fromUtf8(m_dialog->getStringValue("DEV9/Hdd", "HddFile", "DEV9hdd.raw").value().c_str())); connect(m_ui.hddLBA48, &QCheckBox::checkStateChanged, this, &DEV9SettingsWidget::onHddLBA48Changed); UpdateHddSizeUIValues(); connect(m_ui.hddFile, &QLineEdit::textChanged, this, &DEV9SettingsWidget::onHddFileTextChange); connect(m_ui.hddFile, &QLineEdit::editingFinished, this, &DEV9SettingsWidget::onHddFileEdit); connect(m_ui.hddBrowseFile, &QPushButton::clicked, this, &DEV9SettingsWidget::onHddBrowseFileClicked); connect(m_ui.hddSizeSlider, QOverload::of(&QSlider::valueChanged), this, &DEV9SettingsWidget::onHddSizeSlide); SettingWidgetBinder::SettingAccessor::connectValueChanged(m_ui.hddSizeSpinBox, [&]() { onHddSizeAccessorSpin(); }); connect(m_ui.hddCreate, &QPushButton::clicked, this, &DEV9SettingsWidget::onHddCreateClicked); } void DEV9SettingsWidget::onEthEnabledChanged(Qt::CheckState state) { const bool enabled = state == Qt::CheckState::PartiallyChecked ? Host::GetBaseBoolSettingValue("DEV9/Eth", "EthEnable", false) : state; //Populate Eth Device Settings if (enabled) LoadAdapters(); m_ui.ethDevType->setEnabled(enabled); m_ui.ethDevTypeLabel->setEnabled(enabled); m_ui.ethDevLabel->setEnabled(enabled); m_ui.ethDev->setEnabled(enabled); m_ui.ethTabWidget->setEnabled(enabled); } void DEV9SettingsWidget::onEthDeviceTypeChanged(int index) { { QSignalBlocker sb(m_ui.ethDev); m_ui.ethDev->clear(); } Pcsx2Config::DEV9Options::NetApi selectedApi{Pcsx2Config::DEV9Options::NetApi ::Unset}; if (index > 0) { std::vector list = m_adapter_list[static_cast(m_api_list[index])]; const std::string value = m_dialog->getEffectiveStringValue("DEV9/Eth", "EthDevice", ""); for (size_t i = 0; i < list.size(); i++) { m_ui.ethDev->addItem(QString::fromUtf8(list[i].name)); if (list[i].guid == value) m_ui.ethDev->setCurrentIndex(i); } selectedApi = m_api_list[index]; } if (m_dialog->isPerGameSettings()) { if (index == 0) { std::vector list = m_adapter_list[static_cast(m_api_list[index])]; m_ui.ethDev->addItem(tr("Use Global Setting [%1]").arg(QString::fromUtf8(list[0].name))); m_ui.ethDev->setCurrentIndex(0); m_ui.ethDev->setEnabled(false); selectedApi = m_global_api; } else m_ui.ethDev->setEnabled(true); } switch (selectedApi) { #ifdef _WIN32 case Pcsx2Config::DEV9Options::NetApi::TAP: m_adapter_options = TAPAdapter::GetAdapterOptions(); break; #endif case Pcsx2Config::DEV9Options::NetApi::PCAP_Bridged: case Pcsx2Config::DEV9Options::NetApi::PCAP_Switched: m_adapter_options = PCAPAdapter::GetAdapterOptions(); break; case Pcsx2Config::DEV9Options::NetApi::Sockets: m_adapter_options = SocketAdapter::GetAdapterOptions(); break; default: m_adapter_options = AdapterOptions::None; break; } m_ui.ethInterceptDHCPLabel->setEnabled((m_adapter_options & AdapterOptions::DHCP_ForcedOn) == AdapterOptions::None); m_ui.ethInterceptDHCP->setEnabled((m_adapter_options & AdapterOptions::DHCP_ForcedOn) == AdapterOptions::None); onEthDHCPInterceptChanged(m_ui.ethInterceptDHCP->checkState()); } void DEV9SettingsWidget::onEthDeviceChanged(int index) { if (index > 0) { const AdapterEntry& adapter = m_adapter_list[static_cast(m_api_list[m_ui.ethDevType->currentIndex()])][index]; m_dialog->setStringSettingValue("DEV9/Eth", "EthApi", Pcsx2Config::DEV9Options::NetApiNames[static_cast(adapter.type)]); m_dialog->setStringSettingValue("DEV9/Eth", "EthDevice", adapter.guid.c_str()); } else if (m_dialog->isPerGameSettings() && m_ui.ethDevType->currentIndex() == 0 && index == 0) { m_dialog->setStringSettingValue("DEV9/Eth", "EthApi", std::nullopt); m_dialog->setStringSettingValue("DEV9/Eth", "EthDevice", std::nullopt); } } void DEV9SettingsWidget::onEthDHCPInterceptChanged(Qt::CheckState state) { const bool enabled = (state == Qt::CheckState::PartiallyChecked ? Host::GetBaseBoolSettingValue("DEV9/Eth", "InterceptDHCP", false) : state) || ((m_adapter_options & AdapterOptions::DHCP_ForcedOn) == AdapterOptions::DHCP_ForcedOn); // clang-format off const bool ipOverride = (m_adapter_options & AdapterOptions::DHCP_OverrideIP) == AdapterOptions::DHCP_OverrideIP; const bool snOverride = (m_adapter_options & AdapterOptions::DHCP_OverideSubnet) == AdapterOptions::DHCP_OverideSubnet; const bool gwOverride = (m_adapter_options & AdapterOptions::DHCP_OverideGateway) == AdapterOptions::DHCP_OverideGateway; // clang-format on m_ui.ethPS2Addr->setEnabled(enabled && !ipOverride); m_ui.ethPS2AddrLabel->setEnabled(enabled && !ipOverride); m_ui.ethNetMaskLabel->setEnabled(enabled && !snOverride); m_ui.ethNetMaskAuto->setEnabled(enabled && !snOverride); onEthAutoChanged(m_ui.ethNetMaskAuto, m_ui.ethNetMaskAuto->checkState(), m_ui.ethNetMask, "DEV9/Eth", "AutoMask"); m_ui.ethGatewayAddrLabel->setEnabled(enabled && !gwOverride); m_ui.ethGatewayAuto->setEnabled(enabled && !gwOverride); onEthAutoChanged(m_ui.ethGatewayAuto, m_ui.ethGatewayAuto->checkState(), m_ui.ethGatewayAddr, "DEV9/Eth", "AutoGateway"); m_ui.ethDNS1AddrLabel->setEnabled(enabled); m_ui.ethDNS1Mode->setEnabled(enabled); onEthDNSModeChanged(m_ui.ethDNS1Mode, m_ui.ethDNS1Mode->currentIndex(), m_ui.ethDNS1Addr, "DEV9/Eth", "ModeDNS1"); m_ui.ethDNS2AddrLabel->setEnabled(enabled); m_ui.ethDNS2Mode->setEnabled(enabled); onEthDNSModeChanged(m_ui.ethDNS2Mode, m_ui.ethDNS2Mode->currentIndex(), m_ui.ethDNS2Addr, "DEV9/Eth", "ModeDNS2"); } void DEV9SettingsWidget::onEthIPChanged(QLineEdit* sender, const char* section, const char* key) { //Alow clearing a per-game ip setting if (sender->text().isEmpty()) { if (m_dialog->getStringValue(section, key, std::nullopt).has_value()) m_dialog->setStringSettingValue(section, key, std::nullopt); return; } //should already be validated u8 bytes[4]; std::string inputString = sender->text().toUtf8().constData(); sscanf(inputString.c_str(), "%hhu.%hhu.%hhu.%hhu", &bytes[0], &bytes[1], &bytes[2], &bytes[3]); std::string neatStr = StringUtil::StdStringFromFormat("%u.%u.%u.%u", bytes[0], bytes[1], bytes[2], bytes[3]); sender->setText(QString::fromUtf8(neatStr.c_str())); std::string oldval = m_dialog->getStringValue(section, key, "0.0.0.0").value(); if (neatStr != oldval) m_dialog->setStringSettingValue(section, key, neatStr.c_str()); } void DEV9SettingsWidget::onEthAutoChanged(QCheckBox* sender, Qt::CheckState state, QLineEdit* input, const char* section, const char* key) { if (sender->isEnabled()) { const bool manual = !(state == Qt::CheckState::PartiallyChecked ? Host::GetBaseBoolSettingValue(section, key, true) : state); input->setEnabled(manual); } else input->setEnabled(false); } void DEV9SettingsWidget::onEthDNSModeChanged(QComboBox* sender, int index, QLineEdit* input, const char* section, const char* key) { if (sender->isEnabled()) { if (m_dialog->isPerGameSettings()) { if (index == 0) { const std::string value = Host::GetBaseStringSettingValue(section, key, Pcsx2Config::DEV9Options::DnsModeNames[static_cast(Pcsx2Config::DEV9Options::DnsMode::Auto)]); for (int i = 0; Pcsx2Config::DEV9Options::DnsModeNames[i] != nullptr; i++) { if (value == Pcsx2Config::DEV9Options::DnsModeNames[i]) { index = i; break; } } } else index--; } const bool manual = index == static_cast(Pcsx2Config::DEV9Options::DnsMode::Manual); input->setEnabled(manual); } else input->setEnabled(false); } void DEV9SettingsWidget::onEthHostAdd() { HostEntryUi host; host.Desc = "New Host"; host.Enabled = false; AddNewHostConfig(host); //Select new Item const QModelIndex viewIndex = m_ethHosts_proxy->mapFromSource(m_ethHost_model->index(m_ethHost_model->rowCount() - 1, 1)); m_ui.ethHosts->scrollTo(viewIndex, QAbstractItemView::EnsureVisible); m_ui.ethHosts->selectionModel()->setCurrentIndex(viewIndex, QItemSelectionModel::ClearAndSelect); } void DEV9SettingsWidget::onEthHostDel() { if (m_ui.ethHosts->selectionModel()->hasSelection()) { const QModelIndex selectedIndex = m_ui.ethHosts->selectionModel()->currentIndex(); const int modelRow = m_ethHosts_proxy->mapToSource(selectedIndex).row(); DeleteHostConfig(modelRow); } } void DEV9SettingsWidget::onEthHostExport() { std::vector hosts = ListHostsConfig().value(); DEV9DnsHostDialog exportDialog(hosts, this); std::optional> selectedHosts = exportDialog.PromptList(); if (!selectedHosts.has_value()) return; hosts = selectedHosts.value(); if (hosts.size() == 0) return; QString path = QDir::toNativeSeparators(QFileDialog::getSaveFileName(QtUtils::GetRootWidget(this), tr("Hosts File"), "hosts.ini", tr("ini (*.ini)"), nullptr)); if (path.isEmpty()) return; std::unique_ptr exportFile = std::make_unique(path.toUtf8().constData()); //Count is not exported for (size_t i = 0; i < hosts.size(); i++) { std::string section = "Host" + std::to_string(i); HostEntryUi entry = hosts[i]; // clang-format off exportFile->SetStringValue(section.c_str(), "Url", entry.Url.c_str()); exportFile->SetStringValue(section.c_str(), "Desc", entry.Desc.c_str()); exportFile->SetStringValue(section.c_str(), "Address", entry.Address.c_str()); exportFile->SetBoolValue(section.c_str(), "Enabled", entry.Enabled); // clang-format on } exportFile->Save(); QMessageBox::information(this, tr("DNS Hosts"), tr("Exported Successfully"), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); } void DEV9SettingsWidget::onEthHostImport() { std::vector hosts; QString path = QDir::toNativeSeparators(QFileDialog::getOpenFileName(QtUtils::GetRootWidget(this), tr("Hosts File"), "hosts.ini", tr("ini (*.ini)"), nullptr)); if (path.isEmpty()) return; std::unique_ptr importFile = std::make_unique(path.toUtf8().constData()); if (!importFile->Load()) { QMessageBox::warning(this, tr("DNS Hosts"), tr("Failed to open file"), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); return; } size_t count = 0; while (true) { std::string section = "Host" + std::to_string(count); HostEntryUi entry; entry.Url = importFile->GetStringValue(section.c_str(), "Url"); if (entry.Url.empty()) break; // clang-format off entry.Desc = importFile->GetStringValue(section.c_str(), "Desc"); entry.Address = importFile->GetStringValue(section.c_str(), "Address"); entry.Enabled = importFile->GetBoolValue (section.c_str(), "Enabled"); // clang-format on hosts.push_back(entry); count++; } if (hosts.size() == 0) { QMessageBox::warning(this, tr("DNS Hosts"), tr("No Hosts in file"), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); return; } DEV9DnsHostDialog exportDialog(hosts, this); std::optional> selectedHosts = exportDialog.PromptList(); if (!selectedHosts.has_value()) return; hosts = selectedHosts.value(); if (hosts.size() == 0) return; for (size_t i = 0; i < hosts.size(); i++) AddNewHostConfig(hosts[i]); QMessageBox::information(this, tr("DNS Hosts"), tr("Imported Successfully"), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); } void DEV9SettingsWidget::onEthHostPerGame() { const std::optional hostLengthOpt = m_dialog->getIntValue("DEV9/Eth/Hosts", "Count", std::nullopt); if (!hostLengthOpt.has_value()) { QMessageBox::StandardButton ret = QMessageBox::question(this, tr("Per Game Host list"), tr("Copy global settings?"), QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::No | QMessageBox::StandardButton::Cancel, QMessageBox::StandardButton::Yes); switch (ret) { case QMessageBox::StandardButton::No: m_dialog->setIntSettingValue("DEV9/Eth/Hosts", "Count", 0); break; case QMessageBox::StandardButton::Yes: { m_dialog->setIntSettingValue("DEV9/Eth/Hosts", "Count", 0); std::vector hosts = ListBaseHostsConfig(); for (size_t i = 0; i < hosts.size(); i++) AddNewHostConfig(hosts[i]); break; } case QMessageBox::StandardButton::Cancel: return; default: return; } } else { QMessageBox::StandardButton ret = QMessageBox::question(this, tr("Per Game Host list"), tr("Delete per game host list?"), QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::Cancel, QMessageBox::StandardButton::Yes); if (ret == QMessageBox::StandardButton::Yes) { const int hostLength = CountHostsConfig(); for (int i = hostLength - 1; i >= 0; i--) DeleteHostConfig(i); } m_dialog->setIntSettingValue("DEV9/Eth/Hosts", nullptr, std::nullopt); } RefreshHostList(); } void DEV9SettingsWidget::onEthHostEdit(QStandardItem* item) { const int row = item->row(); std::string section = "DEV9/Eth/Hosts/Host" + std::to_string(row); switch (item->column()) { case 0: //Name m_dialog->setStringSettingValue(section.c_str(), "Desc", item->text().toUtf8().constData()); break; case 1: //URL m_dialog->setStringSettingValue(section.c_str(), "Url", item->text().toUtf8().constData()); break; case 2: //IP m_dialog->setStringSettingValue(section.c_str(), "Address", item->text().toUtf8().constData()); break; case 3: //Enabled m_dialog->setBoolSettingValue(section.c_str(), "Enabled", item->checkState() == Qt::CheckState::Checked); break; default: break; } } void DEV9SettingsWidget::onHddEnabledChanged(Qt::CheckState state) { const bool enabled = state == Qt::CheckState::PartiallyChecked ? Host::GetBaseBoolSettingValue("DEV9/Hdd", "HddEnable", false) : state; m_ui.hddFile->setEnabled(enabled); m_ui.hddFileLabel->setEnabled(enabled); m_ui.hddBrowseFile->setEnabled(enabled); m_ui.hddCreate->setEnabled(enabled); UpdateHddSizeUIEnabled(); } void DEV9SettingsWidget::onHddBrowseFileClicked() { QString path = QDir::toNativeSeparators(QFileDialog::getSaveFileName(QtUtils::GetRootWidget(this), tr("HDD Image File"), !m_ui.hddFile->text().isEmpty() ? m_ui.hddFile->text() : (!m_ui.hddFile->placeholderText().isEmpty() ? m_ui.hddFile->placeholderText() : "DEV9hdd.raw"), tr("HDD (*.raw)"), nullptr, QFileDialog::DontConfirmOverwrite)); if (path.isEmpty()) return; m_ui.hddFile->setText(path); m_ui.hddFile->editingFinished(); } void DEV9SettingsWidget::onHddFileTextChange() { UpdateHddSizeUIEnabled(); // Force update so user doesn't have to exit text box std::string hddPath(m_ui.hddFile->text().toStdString()); if (hddPath.empty()) UpdateHddSizeUIValues(); } void DEV9SettingsWidget::onHddFileEdit() { // Check if file exists, if so set HddSize to correct value. // Also save the hddPath setting std::string hddPath(m_ui.hddFile->text().toStdString()); if (hddPath.empty()) m_dialog->setStringSettingValue("DEV9/Hdd", "HddFile", std::nullopt); else m_dialog->setStringSettingValue("DEV9/Hdd", "HddFile", hddPath.c_str()); UpdateHddSizeUIValues(); } void DEV9SettingsWidget::onHddSizeSlide(int i) { QSignalBlocker sb(m_ui.hddSizeSpinBox); m_ui.hddSizeSpinBox->setValue(i); } void DEV9SettingsWidget::onHddSizeAccessorSpin() { QSignalBlocker sb(m_ui.hddSizeSlider); m_ui.hddSizeSlider->setValue(m_ui.hddSizeSpinBox->value()); } void DEV9SettingsWidget::onHddLBA48Changed(Qt::CheckState state) { m_ui.hddSizeSlider->setMaximum((state != Qt::Unchecked) ? 2000 : 120); m_ui.hddSizeSpinBox->setMaximum((state != Qt::Unchecked) ? 2000 : 120); m_ui.hddSizeMaxLabel->setText((state != Qt::Unchecked) ? tr("2000") : tr("120")); // Bump up min size to have ticks align with 100GiB sizes m_ui.hddSizeSlider->setMinimum((state != Qt::Unchecked) ? 100 : 40); m_ui.hddSizeSpinBox->setMinimum((state != Qt::Unchecked) ? 100 : 40); m_ui.hddSizeMinLabel->setText((state != Qt::Unchecked) ? tr("100") : tr("40")); m_ui.hddSizeSlider->setTickInterval((state != Qt::Unchecked) ? 100 : 5); } void DEV9SettingsWidget::onHddCreateClicked() { //Do the thing std::string hddPath(m_ui.hddFile->text().toStdString()); const u64 sizeBytes = (u64)m_ui.hddSizeSpinBox->value() * (u64)(1024 * 1024 * 1024); if (sizeBytes == 0 || hddPath.empty()) { QMessageBox::warning(this, QObject::tr("HDD Creator"), QObject::tr("Failed to create HDD image"), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); return; } if (!Path::IsAbsolute(hddPath)) hddPath = Path::Combine(EmuFolders::Settings, hddPath); if (FileSystem::FileExists(hddPath.c_str())) { QMessageBox::StandardButton selection = QMessageBox::question(this, tr("Overwrite File?"), tr("HDD image \"%1\" already exists.\n\n" "Do you want to overwrite?") .arg(QString::fromStdString(hddPath)), QMessageBox::Yes | QMessageBox::No); if (selection == QMessageBox::No) return; else FileSystem::DeleteFilePath(hddPath.c_str()); } HddCreateQt hddCreator(this); hddCreator.filePath = std::move(hddPath); hddCreator.neededSize = sizeBytes; hddCreator.Start(); if (!hddCreator.errored) { QMessageBox::information(this, tr("HDD Creator"), tr("HDD image created"), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); } } void DEV9SettingsWidget::UpdateHddSizeUIEnabled() { std::string hddPath(m_ui.hddFile->text().toStdString()); bool enableSizeUI; if (m_dialog->isPerGameSettings() && hddPath.empty()) enableSizeUI = false; else enableSizeUI = m_ui.hddFile->isEnabled(); m_ui.hddLBA48->setEnabled(enableSizeUI); m_ui.hddSizeLabel->setEnabled(enableSizeUI); m_ui.hddSizeSlider->setEnabled(enableSizeUI); m_ui.hddSizeMaxLabel->setEnabled(enableSizeUI); m_ui.hddSizeMinLabel->setEnabled(enableSizeUI); m_ui.hddSizeSpinBox->setEnabled(enableSizeUI); } void DEV9SettingsWidget::UpdateHddSizeUIValues() { std::string hddPath(m_ui.hddFile->text().toStdString()); if (m_dialog->isPerGameSettings() && hddPath.empty()) hddPath = m_ui.hddFile->placeholderText().toStdString(); if (!Path::IsAbsolute(hddPath)) hddPath = Path::Combine(EmuFolders::Settings, hddPath); if (!FileSystem::FileExists(hddPath.c_str())) return; const s64 size = FileSystem::GetPathFileSize(hddPath.c_str()); if (size < 0) return; if (size > static_cast(120) * 1024 * 1024 * 1024) m_ui.hddLBA48->setChecked(true); else m_ui.hddLBA48->setChecked(false); const int sizeGB = size / 1024 / 1024 / 1024; QSignalBlocker sb1(m_ui.hddSizeSpinBox); QSignalBlocker sb2(m_ui.hddSizeSlider); m_ui.hddSizeSpinBox->setValue(sizeGB); m_ui.hddSizeSlider->setValue(sizeGB); } void DEV9SettingsWidget::showEvent(QShowEvent* event) { QWidget::showEvent(event); //Update the ethernet UI, as not done by constructor if (m_firstShow) onEthEnabledChanged(m_ui.ethEnabled->checkState()); if (m_adaptersLoaded) { //The API combobox dosn't set the EthApi field, that is performed by the device combobox (in addition to saving the device) //This means that this setting can get out of sync with true value, so revert to that if the ui is closed and opened const std::string value = m_dialog->getStringValue("DEV9/Eth", "EthApi", Pcsx2Config::DEV9Options::NetApiNames[static_cast(Pcsx2Config::DEV9Options::NetApi::Unset)]).value(); //SignalBlocker to prevent saving a value already in the config file QSignalBlocker sb(m_ui.ethDev); for (int i = 0; m_api_namelist[i] != nullptr; i++) { if (value == m_api_valuelist[i]) { m_ui.ethDevType->setCurrentIndex(i); break; } } } m_firstShow = false; } /* * QtUtils::ResizeColumnsForTableView() needs the widget to already be the correct size * Doing this in our resizeEvent (like GameListWidget does) dosn't work if the ui is * hidden, maybe because our table is nested within group & tab widgets * We could also listern to out show event, but we also need to listern to the tab * changed signal, in the event that another tab is selected when our ui is shown * * Instead, lets use an eventFilter to determine exactly when the host table is shown * However, the eventFilter is ran before the widgets event handler, meaning the table * is still the wrong size, so we also need to check the show event */ bool DEV9SettingsWidget::eventFilter(QObject* object, QEvent* event) { if (object == m_ui.ethHosts) { //Check isVisible to avoind an unnessecery call to ResizeColumnsForTableView() if (event->type() == QEvent::Resize && m_ui.ethHosts->isVisible()) QtUtils::ResizeColumnsForTableView(m_ui.ethHosts, {-1, 170, 90, 80}); else if (event->type() == QEvent::Show) QtUtils::ResizeColumnsForTableView(m_ui.ethHosts, {-1, 170, 90, 80}); } return false; } void DEV9SettingsWidget::AddAdapter(const AdapterEntry& adapter) { //divide into seperate adapter lists if (std::find(m_api_list.begin(), m_api_list.end(), adapter.type) == m_api_list.end()) m_api_list.push_back(adapter.type); const u32 idx = static_cast(adapter.type); while (m_adapter_list.size() <= idx) { //Add blank adapter AdapterEntry blankAdapter; blankAdapter.guid = ""; blankAdapter.name = ""; blankAdapter.type = static_cast(m_adapter_list.size()); m_adapter_list.push_back({blankAdapter}); } m_adapter_list[idx].push_back(adapter); } void DEV9SettingsWidget::LoadAdapters() { if (m_adaptersLoaded) return; QSignalBlocker sb(m_ui.ethDev); m_api_list.push_back(Pcsx2Config::DEV9Options::NetApi::Unset); for (const AdapterEntry& adapter : PCAPAdapter::GetAdapters()) AddAdapter(adapter); #ifdef _WIN32 for (const AdapterEntry& adapter : TAPAdapter::GetAdapters()) AddAdapter(adapter); #endif for (const AdapterEntry& adapter : SocketAdapter::GetAdapters()) AddAdapter(adapter); std::sort(m_api_list.begin(), m_api_list.end()); for (auto& list : m_adapter_list) std::sort(list.begin(), list.end(), [](const AdapterEntry& a, AdapterEntry& b) { return a.name < b.name; }); for (const Pcsx2Config::DEV9Options::NetApi& na : m_api_list) { m_api_namelist.push_back(s_api_name[static_cast(na)]); m_api_valuelist.push_back(Pcsx2Config::DEV9Options::NetApiNames[static_cast(na)]); } m_api_namelist.push_back(nullptr); m_api_valuelist.push_back(nullptr); //We replace the blank entry with one for global settings if (m_dialog->isPerGameSettings()) { const std::string valueAPI = Host::GetBaseStringSettingValue("DEV9/Eth", "EthApi", Pcsx2Config::DEV9Options::NetApiNames[static_cast(Pcsx2Config::DEV9Options::NetApi::Unset)]); for (int i = 0; Pcsx2Config::DEV9Options::NetApiNames[i] != nullptr; i++) { if (valueAPI == Pcsx2Config::DEV9Options::NetApiNames[i]) { m_global_api = static_cast(i); break; } } std::vector baseList = m_adapter_list[static_cast(m_global_api)]; std::string baseAdapter = " "; const std::string valueGUID = Host::GetBaseStringSettingValue("DEV9/Eth", "EthDevice", ""); for (size_t i = 0; i < baseList.size(); i++) { if (baseList[i].guid == valueGUID) { baseAdapter = baseList[i].name; break; } } m_adapter_list[static_cast(Pcsx2Config::DEV9Options::NetApi::Unset)][0].name = baseAdapter; } if (m_dialog->isPerGameSettings()) m_ui.ethDevType->addItem(tr("Use Global Setting [%1]").arg(QString::fromUtf8(Pcsx2Config::DEV9Options::NetApiNames[static_cast(m_global_api)]))); else m_ui.ethDevType->addItem(qApp->translate("DEV9SettingsWidget", m_api_namelist[0])); for (int i = 1; m_api_namelist[i] != nullptr; i++) m_ui.ethDevType->addItem(qApp->translate("DEV9SettingsWidget", m_api_namelist[i])); const std::string value = m_dialog->getStringValue("DEV9/Eth", "EthApi", Pcsx2Config::DEV9Options::NetApiNames[static_cast(Pcsx2Config::DEV9Options::NetApi::Unset)]).value(); for (int i = 0; m_api_namelist[i] != nullptr; i++) { if (value == m_api_valuelist[i]) { m_ui.ethDevType->setCurrentIndex(i); break; } } //onEthDeviceTypeChanged gets called automatically m_adaptersLoaded = true; } void DEV9SettingsWidget::RefreshHostList() { while (m_ethHost_model->rowCount() > 0) m_ethHost_model->removeRow(0); bool enableHostsUi; std::vector hosts; if (m_dialog->isPerGameSettings()) { m_ui.ethHostPerGame->setVisible(true); std::optional> hostsOpt = ListHostsConfig(); if (hostsOpt.has_value()) { m_ui.ethHostPerGame->setText(tr("Use Global")); hosts = hostsOpt.value(); enableHostsUi = true; } else { m_ui.ethHostPerGame->setText(tr("Override")); hosts = ListBaseHostsConfig(); enableHostsUi = false; } } else { m_ui.ethHostPerGame->setVisible(false); hosts = ListHostsConfig().value(); enableHostsUi = true; } m_ui.ethHosts->setEnabled(enableHostsUi); m_ui.ethHostAdd->setEnabled(enableHostsUi); m_ui.ethHostDel->setEnabled(enableHostsUi); m_ui.ethHostExport->setEnabled(enableHostsUi); m_ui.ethHostImport->setEnabled(enableHostsUi); //Load list for (size_t i = 0; i < hosts.size(); i++) { HostEntryUi entry = hosts[i]; const int row = m_ethHost_model->rowCount(); m_ethHost_model->insertRow(row); QSignalBlocker sb(m_ethHost_model); QStandardItem* nameItem = new QStandardItem(); nameItem->setText(QString::fromStdString(entry.Desc)); m_ethHost_model->setItem(row, 0, nameItem); QStandardItem* urlItem = new QStandardItem(); urlItem->setText(QString::fromStdString(entry.Url)); m_ethHost_model->setItem(row, 1, urlItem); QStandardItem* addressItem = new QStandardItem(); addressItem->setText(QString::fromStdString(entry.Address)); m_ethHost_model->setItem(row, 2, addressItem); QStandardItem* enabledItem = new QStandardItem(); enabledItem->setEditable(false); enabledItem->setCheckable(true); enabledItem->setCheckState(entry.Enabled ? Qt::CheckState::Checked : Qt::CheckState::Unchecked); m_ethHost_model->setItem(row, 3, enabledItem); } m_ui.ethHosts->sortByColumn(0, Qt::AscendingOrder); } int DEV9SettingsWidget::CountHostsConfig() { return m_dialog->getIntValue("DEV9/Eth/Hosts", "Count", 0).value(); } std::optional> DEV9SettingsWidget::ListHostsConfig() { std::vector hosts; std::optional hostLengthOpt; if (m_dialog->isPerGameSettings()) { hostLengthOpt = m_dialog->getIntValue("DEV9/Eth/Hosts", "Count", std::nullopt); if (!hostLengthOpt.has_value()) return std::nullopt; } else hostLengthOpt = m_dialog->getIntValue("DEV9/Eth/Hosts", "Count", 0); const int hostLength = hostLengthOpt.value(); for (int i = 0; i < hostLength; i++) { std::string section = "DEV9/Eth/Hosts/Host" + std::to_string(i); HostEntryUi entry; entry.Url = m_dialog->getStringValue(section.c_str(), "Url", "").value(); entry.Desc = m_dialog->getStringValue(section.c_str(), "Desc", "").value(); entry.Address = m_dialog->getStringValue(section.c_str(), "Address", "").value(); entry.Enabled = m_dialog->getBoolValue(section.c_str(), "Enabled", false).value(); hosts.push_back(entry); } return hosts; } std::vector DEV9SettingsWidget::ListBaseHostsConfig() { std::vector hosts; const int hostLength = Host::GetBaseIntSettingValue("DEV9/Eth/Hosts", "Count", 0); for (int i = 0; i < hostLength; i++) { std::string section = "DEV9/Eth/Hosts/Host" + std::to_string(i); HostEntryUi entry; entry.Url = Host::GetBaseStringSettingValue(section.c_str(), "Url", ""); entry.Desc = Host::GetBaseStringSettingValue(section.c_str(), "Desc", ""); entry.Address = Host::GetBaseStringSettingValue(section.c_str(), "Address", ""); entry.Enabled = Host::GetBaseBoolSettingValue(section.c_str(), "Enabled", false); hosts.push_back(entry); } return hosts; } void DEV9SettingsWidget::AddNewHostConfig(const HostEntryUi& host) { const int hostLength = CountHostsConfig(); std::string section = "DEV9/Eth/Hosts/Host" + std::to_string(hostLength); // clang-format off m_dialog->setStringSettingValue(section.c_str(), "Url", host.Url.c_str()); m_dialog->setStringSettingValue(section.c_str(), "Desc", host.Desc.c_str()); m_dialog->setStringSettingValue(section.c_str(), "Address", host.Address.c_str()); m_dialog->setBoolSettingValue (section.c_str(), "Enabled", host.Enabled); // clang-format on m_dialog->setIntSettingValue("DEV9/Eth/Hosts", "Count", hostLength + 1); RefreshHostList(); } void DEV9SettingsWidget::DeleteHostConfig(int index) { const int hostLength = CountHostsConfig(); //Shuffle entries down to ovewrite deleted entry for (int i = index; i < hostLength - 1; i++) { std::string section = "DEV9/Eth/Hosts/Host" + std::to_string(i); std::string sectionAhead = "DEV9/Eth/Hosts/Host" + std::to_string(i + 1); // clang-format off m_dialog->setStringSettingValue(section.c_str(), "Url", m_dialog->getStringValue(sectionAhead.c_str(), "Url", "").value().c_str()); m_dialog->setStringSettingValue(section.c_str(), "Desc", m_dialog->getStringValue(sectionAhead.c_str(), "Desc", "").value().c_str()); m_dialog->setStringSettingValue(section.c_str(), "Address", m_dialog->getStringValue(sectionAhead.c_str(), "Address", "0.0.0.0").value().c_str()); m_dialog->setBoolSettingValue (section.c_str(), "Enabled", m_dialog->getBoolValue (sectionAhead.c_str(), "Enabled", false).value()); // clang-format on } //Delete last entry std::string section = "DEV9/Eth/Hosts/Host" + std::to_string(hostLength - 1); //Specifying a value of nullopt will delete the key //if the key is a nullptr, the whole section is deleted m_dialog->setStringSettingValue(section.c_str(), nullptr, std::nullopt); m_dialog->setIntSettingValue("DEV9/Eth/Hosts", "Count", hostLength - 1); RefreshHostList(); } DEV9SettingsWidget::~DEV9SettingsWidget() = default;