// Copyright 2018 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "DolphinQt/Settings/GameCubePane.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Common/Assert.h" #include "Common/CommonPaths.h" #include "Common/Config/Config.h" #include "Common/FileUtil.h" #include "Common/MsgHandler.h" #include "Core/Config/MainSettings.h" #include "Core/ConfigManager.h" #include "Core/Core.h" #include "Core/HW/EXI/EXI.h" #include "Core/HW/GCMemcard/GCMemcard.h" #include "Core/NetPlayServer.h" #include "DolphinQt/Config/Mapping/MappingWindow.h" #include "DolphinQt/GCMemcardManager.h" #include "DolphinQt/QtUtils/DolphinFileDialog.h" #include "DolphinQt/QtUtils/ModalMessageBox.h" #include "DolphinQt/QtUtils/NonDefaultQPushButton.h" #include "DolphinQt/QtUtils/SignalBlocking.h" #include "DolphinQt/Settings.h" #include "DolphinQt/Settings/BroadbandAdapterSettingsDialog.h" enum { SLOT_A_INDEX, SLOT_B_INDEX, SLOT_SP1_INDEX, SLOT_COUNT }; GameCubePane::GameCubePane() { CreateWidgets(); LoadSettings(); ConnectWidgets(); } void GameCubePane::CreateWidgets() { using ExpansionInterface::EXIDeviceType; QVBoxLayout* layout = new QVBoxLayout(this); // IPL Settings QGroupBox* ipl_box = new QGroupBox(tr("IPL Settings"), this); QVBoxLayout* ipl_box_layout = new QVBoxLayout(ipl_box); ipl_box->setLayout(ipl_box_layout); m_skip_main_menu = new QCheckBox(tr("Skip Main Menu"), ipl_box); ipl_box_layout->addWidget(m_skip_main_menu); QFormLayout* ipl_language_layout = new QFormLayout; ipl_language_layout->setFormAlignment(Qt::AlignLeft | Qt::AlignTop); ipl_language_layout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); ipl_box_layout->addLayout(ipl_language_layout); m_language_combo = new QComboBox(ipl_box); m_language_combo->setCurrentIndex(-1); ipl_language_layout->addRow(tr("System Language:"), m_language_combo); // Add languages for (const auto& entry : {std::make_pair(tr("English"), 0), std::make_pair(tr("German"), 1), std::make_pair(tr("French"), 2), std::make_pair(tr("Spanish"), 3), std::make_pair(tr("Italian"), 4), std::make_pair(tr("Dutch"), 5)}) { m_language_combo->addItem(entry.first, entry.second); } // Device Settings QGroupBox* device_box = new QGroupBox(tr("Device Settings"), this); QGridLayout* device_layout = new QGridLayout(device_box); device_box->setLayout(device_layout); for (ExpansionInterface::Slot slot : ExpansionInterface::SLOTS) { m_slot_combos[slot] = new QComboBox(device_box); m_slot_combos[slot]->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); m_slot_buttons[slot] = new NonDefaultQPushButton(tr("..."), device_box); m_slot_buttons[slot]->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); } // Add slot devices for (const auto device : {EXIDeviceType::None, EXIDeviceType::Dummy, EXIDeviceType::MemoryCard, EXIDeviceType::MemoryCardFolder, EXIDeviceType::Gecko, EXIDeviceType::AGP, EXIDeviceType::Microphone}) { const QString name = tr(fmt::format("{:n}", device).c_str()); const int value = static_cast(device); m_slot_combos[ExpansionInterface::Slot::A]->addItem(name, value); m_slot_combos[ExpansionInterface::Slot::B]->addItem(name, value); } // Add SP1 devices for (const auto device : { EXIDeviceType::None, EXIDeviceType::Dummy, EXIDeviceType::Ethernet, EXIDeviceType::EthernetXLink, #ifdef __APPLE__ EXIDeviceType::EthernetTapServer, #endif }) { m_slot_combos[ExpansionInterface::Slot::SP1]->addItem(tr(fmt::format("{:n}", device).c_str()), static_cast(device)); } device_layout->addWidget(new QLabel(tr("Slot A:")), 0, 0); device_layout->addWidget(m_slot_combos[ExpansionInterface::Slot::A], 0, 1); device_layout->addWidget(m_slot_buttons[ExpansionInterface::Slot::A], 0, 2); device_layout->addWidget(new QLabel(tr("Slot B:")), 1, 0); device_layout->addWidget(m_slot_combos[ExpansionInterface::Slot::B], 1, 1); device_layout->addWidget(m_slot_buttons[ExpansionInterface::Slot::B], 1, 2); device_layout->addWidget(new QLabel(tr("SP1:")), 2, 0); device_layout->addWidget(m_slot_combos[ExpansionInterface::Slot::SP1], 2, 1); device_layout->addWidget(m_slot_buttons[ExpansionInterface::Slot::SP1], 2, 2); #ifdef HAS_LIBMGBA // GBA Settings auto* gba_box = new QGroupBox(tr("GBA Settings"), this); auto* gba_layout = new QGridLayout(gba_box); gba_box->setLayout(gba_layout); int gba_row = 0; m_gba_threads = new QCheckBox(tr("Run GBA Cores in Dedicated Threads")); gba_layout->addWidget(m_gba_threads, gba_row, 0, 1, -1); gba_row++; m_gba_bios_edit = new QLineEdit(); m_gba_browse_bios = new NonDefaultQPushButton(QStringLiteral("...")); gba_layout->addWidget(new QLabel(tr("BIOS:")), gba_row, 0); gba_layout->addWidget(m_gba_bios_edit, gba_row, 1); gba_layout->addWidget(m_gba_browse_bios, gba_row, 2); gba_row++; for (size_t i = 0; i < m_gba_rom_edits.size(); ++i) { m_gba_rom_edits[i] = new QLineEdit(); m_gba_browse_roms[i] = new NonDefaultQPushButton(QStringLiteral("...")); gba_layout->addWidget(new QLabel(tr("Port %1 ROM:").arg(i + 1)), gba_row, 0); gba_layout->addWidget(m_gba_rom_edits[i], gba_row, 1); gba_layout->addWidget(m_gba_browse_roms[i], gba_row, 2); gba_row++; } m_gba_save_rom_path = new QCheckBox(tr("Save in Same Directory as the ROM")); gba_layout->addWidget(m_gba_save_rom_path, gba_row, 0, 1, -1); gba_row++; m_gba_saves_edit = new QLineEdit(); m_gba_browse_saves = new NonDefaultQPushButton(QStringLiteral("...")); gba_layout->addWidget(new QLabel(tr("Saves:")), gba_row, 0); gba_layout->addWidget(m_gba_saves_edit, gba_row, 1); gba_layout->addWidget(m_gba_browse_saves, gba_row, 2); gba_row++; #endif layout->addWidget(ipl_box); layout->addWidget(device_box); #ifdef HAS_LIBMGBA layout->addWidget(gba_box); #endif layout->addStretch(); setLayout(layout); } void GameCubePane::ConnectWidgets() { // IPL Settings connect(m_skip_main_menu, &QCheckBox::stateChanged, this, &GameCubePane::SaveSettings); connect(m_language_combo, qOverload(&QComboBox::currentIndexChanged), this, &GameCubePane::SaveSettings); // Device Settings for (ExpansionInterface::Slot slot : ExpansionInterface::SLOTS) { connect(m_slot_combos[slot], qOverload(&QComboBox::currentIndexChanged), this, [this, slot] { UpdateButton(slot); }); connect(m_slot_combos[slot], qOverload(&QComboBox::currentIndexChanged), this, &GameCubePane::SaveSettings); connect(m_slot_buttons[slot], &QPushButton::clicked, [this, slot] { OnConfigPressed(slot); }); } #ifdef HAS_LIBMGBA // GBA Settings connect(m_gba_threads, &QCheckBox::stateChanged, this, &GameCubePane::SaveSettings); connect(m_gba_bios_edit, &QLineEdit::editingFinished, this, &GameCubePane::SaveSettings); connect(m_gba_browse_bios, &QPushButton::clicked, this, &GameCubePane::BrowseGBABios); connect(m_gba_save_rom_path, &QCheckBox::stateChanged, this, &GameCubePane::SaveRomPathChanged); connect(m_gba_saves_edit, &QLineEdit::editingFinished, this, &GameCubePane::SaveSettings); connect(m_gba_browse_saves, &QPushButton::clicked, this, &GameCubePane::BrowseGBASaves); for (size_t i = 0; i < m_gba_browse_roms.size(); ++i) { connect(m_gba_rom_edits[i], &QLineEdit::editingFinished, this, &GameCubePane::SaveSettings); connect(m_gba_browse_roms[i], &QPushButton::clicked, this, [this, i] { BrowseGBARom(i); }); } #endif // Emulation State connect(&Settings::Instance(), &Settings::EmulationStateChanged, this, &GameCubePane::OnEmulationStateChanged); OnEmulationStateChanged(); } void GameCubePane::OnEmulationStateChanged() { #ifdef HAS_LIBMGBA bool gba_enabled = !NetPlay::IsNetPlayRunning(); m_gba_threads->setEnabled(gba_enabled); m_gba_bios_edit->setEnabled(gba_enabled); m_gba_browse_bios->setEnabled(gba_enabled); m_gba_save_rom_path->setEnabled(gba_enabled); m_gba_saves_edit->setEnabled(gba_enabled); m_gba_browse_saves->setEnabled(gba_enabled); for (size_t i = 0; i < m_gba_browse_roms.size(); ++i) { m_gba_rom_edits[i]->setEnabled(gba_enabled); m_gba_browse_roms[i]->setEnabled(gba_enabled); } #endif } void GameCubePane::UpdateButton(ExpansionInterface::Slot slot) { const auto device = static_cast(m_slot_combos[slot]->currentData().toInt()); bool has_config = false; switch (slot) { case ExpansionInterface::Slot::A: case ExpansionInterface::Slot::B: has_config = (device == ExpansionInterface::EXIDeviceType::MemoryCard || device == ExpansionInterface::EXIDeviceType::AGP || device == ExpansionInterface::EXIDeviceType::Microphone); break; case ExpansionInterface::Slot::SP1: has_config = (device == ExpansionInterface::EXIDeviceType::Ethernet || device == ExpansionInterface::EXIDeviceType::EthernetXLink); break; } m_slot_buttons[slot]->setEnabled(has_config); } void GameCubePane::OnConfigPressed(ExpansionInterface::Slot slot) { const ExpansionInterface::EXIDeviceType device = static_cast(m_slot_combos[slot]->currentData().toInt()); switch (device) { case ExpansionInterface::EXIDeviceType::MemoryCard: BrowseMemcard(slot); return; case ExpansionInterface::EXIDeviceType::AGP: BrowseAGPRom(slot); return; case ExpansionInterface::EXIDeviceType::Microphone: // TODO: convert MappingWindow to use Slot? MappingWindow(this, MappingWindow::Type::MAPPING_GC_MICROPHONE, static_cast(slot)).exec(); return; case ExpansionInterface::EXIDeviceType::Ethernet: { BroadbandAdapterSettingsDialog(this, BroadbandAdapterSettingsDialog::Type::Ethernet).exec(); return; } case ExpansionInterface::EXIDeviceType::EthernetXLink: { BroadbandAdapterSettingsDialog(this, BroadbandAdapterSettingsDialog::Type::XLinkKai).exec(); return; } default: PanicAlertFmt("Unknown settings pressed for {}", device); return; } } void GameCubePane::BrowseMemcard(ExpansionInterface::Slot slot) { ASSERT(ExpansionInterface::IsMemcardSlot(slot)); QString filename = DolphinFileDialog::getSaveFileName( this, tr("Choose a file to open"), QString::fromStdString(File::GetUserPath(D_GCUSER_IDX)), tr("GameCube Memory Cards (*.raw *.gcp)"), 0, QFileDialog::DontConfirmOverwrite); if (filename.isEmpty()) return; QString path_abs = QFileInfo(filename).absoluteFilePath(); // Memcard validity checks if (File::Exists(filename.toStdString())) { auto [error_code, mc] = Memcard::GCMemcard::Open(filename.toStdString()); if (error_code.HasCriticalErrors() || !mc || !mc->IsValid()) { ModalMessageBox::critical( this, tr("Error"), tr("The file\n%1\nis either corrupted or not a GameCube memory card file.\n%2") .arg(filename) .arg(GCMemcardManager::GetErrorMessagesForErrorCode(error_code))); return; } } for (ExpansionInterface::Slot other_slot : ExpansionInterface::MEMCARD_SLOTS) { if (other_slot == slot) continue; bool other_slot_memcard = m_slot_combos[other_slot]->currentData().toInt() == static_cast(ExpansionInterface::EXIDeviceType::MemoryCard); if (other_slot_memcard) { QString path_other = QFileInfo(QString::fromStdString(Config::Get(Config::GetInfoForMemcardPath(other_slot)))) .absoluteFilePath(); if (path_abs == path_other) { ModalMessageBox::critical( this, tr("Error"), tr("The same file can't be used in multiple slots; it is already used by %1.") .arg(QString::fromStdString(fmt::to_string(other_slot)))); return; } } } QString path_old = QFileInfo(QString::fromStdString(Config::Get(Config::GetInfoForMemcardPath(slot)))) .absoluteFilePath(); Config::SetBase(Config::GetInfoForMemcardPath(slot), path_abs.toStdString()); if (Core::IsRunning() && path_abs != path_old) { // ChangeDevice unplugs the device for 1 second, which means that games should notice that // the path has changed and thus the memory card contents have changed ExpansionInterface::ChangeDevice(slot, ExpansionInterface::EXIDeviceType::MemoryCard); } } void GameCubePane::BrowseAGPRom(ExpansionInterface::Slot slot) { ASSERT(ExpansionInterface::IsMemcardSlot(slot)); QString filename = DolphinFileDialog::getSaveFileName( this, tr("Choose a file to open"), QString::fromStdString(File::GetUserPath(D_GCUSER_IDX)), tr("Game Boy Advance Carts (*.gba)"), 0, QFileDialog::DontConfirmOverwrite); if (filename.isEmpty()) return; QString path_abs = QFileInfo(filename).absoluteFilePath(); QString path_old = QFileInfo(QString::fromStdString(Config::Get(Config::GetInfoForAGPCartPath(slot)))) .absoluteFilePath(); Config::SetBase(Config::GetInfoForAGPCartPath(slot), path_abs.toStdString()); if (Core::IsRunning() && path_abs != path_old) { // ChangeDevice unplugs the device for 1 second. For an actual AGP, you can remove the // cartridge without unplugging it, and it's not clear if the AGP software actually notices // that it's been unplugged or the cartridge has changed, but this was done for memcards so // we might as well do it for the AGP too. ExpansionInterface::ChangeDevice(slot, ExpansionInterface::EXIDeviceType::AGP); } } void GameCubePane::BrowseGBABios() { QString file = QDir::toNativeSeparators(DolphinFileDialog::getOpenFileName( this, tr("Select GBA BIOS"), QString::fromStdString(File::GetUserPath(F_GBABIOS_IDX)), tr("All Files (*)"))); if (!file.isEmpty()) { m_gba_bios_edit->setText(file); SaveSettings(); } } void GameCubePane::BrowseGBARom(size_t index) { QString file = QString::fromStdString(GetOpenGBARom({})); if (!file.isEmpty()) { m_gba_rom_edits[index]->setText(file); SaveSettings(); } } void GameCubePane::SaveRomPathChanged() { m_gba_saves_edit->setEnabled(!m_gba_save_rom_path->isChecked()); m_gba_browse_saves->setEnabled(!m_gba_save_rom_path->isChecked()); SaveSettings(); } void GameCubePane::BrowseGBASaves() { QString dir = QDir::toNativeSeparators(DolphinFileDialog::getExistingDirectory( this, tr("Select GBA Saves Path"), QString::fromStdString(File::GetUserPath(D_GBASAVES_IDX)))); if (!dir.isEmpty()) { m_gba_saves_edit->setText(dir); SaveSettings(); } } void GameCubePane::LoadSettings() { // IPL Settings SignalBlocking(m_skip_main_menu)->setChecked(Config::Get(Config::MAIN_SKIP_IPL)); SignalBlocking(m_language_combo) ->setCurrentIndex(m_language_combo->findData(Config::Get(Config::MAIN_GC_LANGUAGE))); bool have_menu = false; for (const std::string dir : {USA_DIR, JAP_DIR, EUR_DIR}) { const auto path = DIR_SEP + dir + DIR_SEP GC_IPL; if (File::Exists(File::GetUserPath(D_GCUSER_IDX) + path) || File::Exists(File::GetSysDirectory() + GC_SYS_DIR + path)) { have_menu = true; break; } } m_skip_main_menu->setEnabled(have_menu); m_skip_main_menu->setToolTip(have_menu ? QString{} : tr("Put IPL ROMs in User/GC/.")); // Device Settings for (ExpansionInterface::Slot slot : ExpansionInterface::SLOTS) { const ExpansionInterface::EXIDeviceType exi_device = Config::Get(Config::GetInfoForEXIDevice(slot)); SignalBlocking(m_slot_combos[slot]) ->setCurrentIndex(m_slot_combos[slot]->findData(static_cast(exi_device))); UpdateButton(slot); } #ifdef HAS_LIBMGBA // GBA Settings SignalBlocking(m_gba_threads)->setChecked(Config::Get(Config::MAIN_GBA_THREADS)); SignalBlocking(m_gba_bios_edit) ->setText(QString::fromStdString(File::GetUserPath(F_GBABIOS_IDX))); SignalBlocking(m_gba_save_rom_path)->setChecked(Config::Get(Config::MAIN_GBA_SAVES_IN_ROM_PATH)); SignalBlocking(m_gba_saves_edit) ->setText(QString::fromStdString(File::GetUserPath(D_GBASAVES_IDX))); for (size_t i = 0; i < m_gba_rom_edits.size(); ++i) { SignalBlocking(m_gba_rom_edits[i]) ->setText(QString::fromStdString(Config::Get(Config::MAIN_GBA_ROM_PATHS[i]))); } #endif } void GameCubePane::SaveSettings() { Config::ConfigChangeCallbackGuard config_guard; // IPL Settings Config::SetBaseOrCurrent(Config::MAIN_SKIP_IPL, m_skip_main_menu->isChecked()); Config::SetBaseOrCurrent(Config::MAIN_GC_LANGUAGE, m_language_combo->currentData().toInt()); // Device Settings for (ExpansionInterface::Slot slot : ExpansionInterface::SLOTS) { const auto dev = static_cast(m_slot_combos[slot]->currentData().toInt()); const ExpansionInterface::EXIDeviceType current_exi_device = Config::Get(Config::GetInfoForEXIDevice(slot)); if (Core::IsRunning() && current_exi_device != dev) { ExpansionInterface::ChangeDevice(slot, dev); } Config::SetBaseOrCurrent(Config::GetInfoForEXIDevice(slot), dev); } #ifdef HAS_LIBMGBA // GBA Settings if (!NetPlay::IsNetPlayRunning()) { Config::SetBaseOrCurrent(Config::MAIN_GBA_THREADS, m_gba_threads->isChecked()); Config::SetBaseOrCurrent(Config::MAIN_GBA_BIOS_PATH, m_gba_bios_edit->text().toStdString()); Config::SetBaseOrCurrent(Config::MAIN_GBA_SAVES_IN_ROM_PATH, m_gba_save_rom_path->isChecked()); Config::SetBaseOrCurrent(Config::MAIN_GBA_SAVES_PATH, m_gba_saves_edit->text().toStdString()); File::SetUserPath(F_GBABIOS_IDX, Config::Get(Config::MAIN_GBA_BIOS_PATH)); File::SetUserPath(D_GBASAVES_IDX, Config::Get(Config::MAIN_GBA_SAVES_PATH)); for (size_t i = 0; i < m_gba_rom_edits.size(); ++i) { Config::SetBaseOrCurrent(Config::MAIN_GBA_ROM_PATHS[i], m_gba_rom_edits[i]->text().toStdString()); } auto server = Settings::Instance().GetNetPlayServer(); if (server) server->SetGBAConfig(server->GetGBAConfig(), true); } #endif LoadSettings(); } std::string GameCubePane::GetOpenGBARom(std::string_view title) { QString caption = tr("Select GBA ROM"); if (!title.empty()) caption += QStringLiteral(": %1").arg(QString::fromStdString(std::string(title))); return QDir::toNativeSeparators( DolphinFileDialog::getOpenFileName( nullptr, caption, QString(), tr("Game Boy Advance ROMs (*.gba *.gbc *.gb *.7z *.zip *.agb *.mb *.rom *.bin);;" "All Files (*)"))) .toStdString(); }