GameCubePane.cpp 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837
  1. // Copyright 2018 Dolphin Emulator Project
  2. // SPDX-License-Identifier: GPL-2.0-or-later
  3. #include "DolphinQt/Settings/GameCubePane.h"
  4. #include <QCheckBox>
  5. #include <QComboBox>
  6. #include <QFileDialog>
  7. #include <QFileInfo>
  8. #include <QFormLayout>
  9. #include <QGridLayout>
  10. #include <QGroupBox>
  11. #include <QHBoxLayout>
  12. #include <QInputDialog>
  13. #include <QLabel>
  14. #include <QLineEdit>
  15. #include <QPushButton>
  16. #include <QVBoxLayout>
  17. #include <array>
  18. #include <utility>
  19. #include "Common/Assert.h"
  20. #include "Common/CommonPaths.h"
  21. #include "Common/Config/Config.h"
  22. #include "Common/FileUtil.h"
  23. #include "Common/MsgHandler.h"
  24. #include "Core/Config/MainSettings.h"
  25. #include "Core/ConfigManager.h"
  26. #include "Core/Core.h"
  27. #include "Core/HW/EXI/EXI.h"
  28. #include "Core/HW/GCMemcard/GCMemcard.h"
  29. #include "Core/NetPlayServer.h"
  30. #include "Core/System.h"
  31. #include "DolphinQt/Config/Mapping/MappingWindow.h"
  32. #include "DolphinQt/GCMemcardManager.h"
  33. #include "DolphinQt/QtUtils/DolphinFileDialog.h"
  34. #include "DolphinQt/QtUtils/ModalMessageBox.h"
  35. #include "DolphinQt/QtUtils/NonDefaultQPushButton.h"
  36. #include "DolphinQt/QtUtils/SetWindowDecorations.h"
  37. #include "DolphinQt/QtUtils/SignalBlocking.h"
  38. #include "DolphinQt/Settings.h"
  39. #include "DolphinQt/Settings/BroadbandAdapterSettingsDialog.h"
  40. constexpr std::initializer_list<ExpansionInterface::Slot> GUI_SLOTS = {
  41. ExpansionInterface::Slot::A, ExpansionInterface::Slot::B, ExpansionInterface::Slot::SP1};
  42. GameCubePane::GameCubePane()
  43. {
  44. CreateWidgets();
  45. LoadSettings();
  46. ConnectWidgets();
  47. }
  48. void GameCubePane::CreateWidgets()
  49. {
  50. using ExpansionInterface::EXIDeviceType;
  51. QVBoxLayout* layout = new QVBoxLayout(this);
  52. // IPL Settings
  53. QGroupBox* ipl_box = new QGroupBox(tr("IPL Settings"), this);
  54. QVBoxLayout* ipl_box_layout = new QVBoxLayout(ipl_box);
  55. ipl_box->setLayout(ipl_box_layout);
  56. m_skip_main_menu = new QCheckBox(tr("Skip Main Menu"), ipl_box);
  57. ipl_box_layout->addWidget(m_skip_main_menu);
  58. QFormLayout* ipl_language_layout = new QFormLayout;
  59. ipl_language_layout->setFormAlignment(Qt::AlignLeft | Qt::AlignTop);
  60. ipl_language_layout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
  61. ipl_box_layout->addLayout(ipl_language_layout);
  62. m_language_combo = new QComboBox(ipl_box);
  63. m_language_combo->setCurrentIndex(-1);
  64. ipl_language_layout->addRow(tr("System Language:"), m_language_combo);
  65. // Add languages
  66. for (const auto& entry : {std::make_pair(tr("English"), 0), std::make_pair(tr("German"), 1),
  67. std::make_pair(tr("French"), 2), std::make_pair(tr("Spanish"), 3),
  68. std::make_pair(tr("Italian"), 4), std::make_pair(tr("Dutch"), 5)})
  69. {
  70. m_language_combo->addItem(entry.first, entry.second);
  71. }
  72. // Device Settings
  73. QGroupBox* device_box = new QGroupBox(tr("Device Settings"), this);
  74. QGridLayout* device_layout = new QGridLayout(device_box);
  75. device_box->setLayout(device_layout);
  76. for (ExpansionInterface::Slot slot : GUI_SLOTS)
  77. {
  78. m_slot_combos[slot] = new QComboBox(device_box);
  79. m_slot_combos[slot]->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed);
  80. m_slot_buttons[slot] = new NonDefaultQPushButton(tr("..."), device_box);
  81. m_slot_buttons[slot]->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
  82. }
  83. for (ExpansionInterface::Slot slot : ExpansionInterface::MEMCARD_SLOTS)
  84. {
  85. m_memcard_path_layouts[slot] = new QHBoxLayout();
  86. m_memcard_path_labels[slot] = new QLabel(tr("Memory Card Path:"));
  87. m_memcard_paths[slot] = new QLineEdit();
  88. m_memcard_path_layouts[slot]->addWidget(m_memcard_path_labels[slot]);
  89. m_memcard_path_layouts[slot]->addWidget(m_memcard_paths[slot]);
  90. m_agp_path_layouts[slot] = new QHBoxLayout();
  91. m_agp_path_labels[slot] = new QLabel(tr("GBA Cartridge Path:"));
  92. m_agp_paths[slot] = new QLineEdit();
  93. m_agp_path_layouts[slot]->addWidget(m_agp_path_labels[slot]);
  94. m_agp_path_layouts[slot]->addWidget(m_agp_paths[slot]);
  95. m_gci_path_layouts[slot] = new QVBoxLayout();
  96. m_gci_path_labels[slot] = new QLabel(tr("GCI Folder Path:"));
  97. m_gci_override_labels[slot] =
  98. new QLabel(tr("Warning: A GCI folder override path is currently configured for this slot. "
  99. "Adjusting the GCI path here will have no effect."));
  100. m_gci_override_labels[slot]->setHidden(true);
  101. m_gci_override_labels[slot]->setWordWrap(true);
  102. m_gci_paths[slot] = new QLineEdit();
  103. auto* hlayout = new QHBoxLayout();
  104. hlayout->addWidget(m_gci_path_labels[slot]);
  105. hlayout->addWidget(m_gci_paths[slot]);
  106. m_gci_path_layouts[slot]->addWidget(m_gci_override_labels[slot]);
  107. m_gci_path_layouts[slot]->addLayout(hlayout);
  108. }
  109. // Add slot devices
  110. for (const auto device : {EXIDeviceType::None, EXIDeviceType::Dummy, EXIDeviceType::MemoryCard,
  111. EXIDeviceType::MemoryCardFolder, EXIDeviceType::Gecko,
  112. EXIDeviceType::AGP, EXIDeviceType::Microphone})
  113. {
  114. const QString name = tr(fmt::format("{:n}", device).c_str());
  115. const int value = static_cast<int>(device);
  116. m_slot_combos[ExpansionInterface::Slot::A]->addItem(name, value);
  117. m_slot_combos[ExpansionInterface::Slot::B]->addItem(name, value);
  118. }
  119. // Add SP1 devices
  120. for (const auto device : {
  121. EXIDeviceType::None,
  122. EXIDeviceType::Dummy,
  123. EXIDeviceType::Ethernet,
  124. EXIDeviceType::EthernetXLink,
  125. EXIDeviceType::EthernetTapServer,
  126. EXIDeviceType::EthernetBuiltIn,
  127. EXIDeviceType::ModemTapServer,
  128. })
  129. {
  130. m_slot_combos[ExpansionInterface::Slot::SP1]->addItem(tr(fmt::format("{:n}", device).c_str()),
  131. static_cast<int>(device));
  132. }
  133. {
  134. int row = 0;
  135. device_layout->addWidget(new QLabel(tr("Slot A:")), row, 0);
  136. device_layout->addWidget(m_slot_combos[ExpansionInterface::Slot::A], row, 1);
  137. device_layout->addWidget(m_slot_buttons[ExpansionInterface::Slot::A], row, 2);
  138. ++row;
  139. device_layout->addLayout(m_memcard_path_layouts[ExpansionInterface::Slot::A], row, 0, 1, 3);
  140. ++row;
  141. device_layout->addLayout(m_agp_path_layouts[ExpansionInterface::Slot::A], row, 0, 1, 3);
  142. ++row;
  143. device_layout->addLayout(m_gci_path_layouts[ExpansionInterface::Slot::A], row, 0, 1, 3);
  144. ++row;
  145. device_layout->addWidget(new QLabel(tr("Slot B:")), row, 0);
  146. device_layout->addWidget(m_slot_combos[ExpansionInterface::Slot::B], row, 1);
  147. device_layout->addWidget(m_slot_buttons[ExpansionInterface::Slot::B], row, 2);
  148. ++row;
  149. device_layout->addLayout(m_memcard_path_layouts[ExpansionInterface::Slot::B], row, 0, 1, 3);
  150. ++row;
  151. device_layout->addLayout(m_agp_path_layouts[ExpansionInterface::Slot::B], row, 0, 1, 3);
  152. ++row;
  153. device_layout->addLayout(m_gci_path_layouts[ExpansionInterface::Slot::B], row, 0, 1, 3);
  154. ++row;
  155. device_layout->addWidget(new QLabel(tr("SP1:")), row, 0);
  156. device_layout->addWidget(m_slot_combos[ExpansionInterface::Slot::SP1], row, 1);
  157. device_layout->addWidget(m_slot_buttons[ExpansionInterface::Slot::SP1], row, 2);
  158. }
  159. #ifdef HAS_LIBMGBA
  160. // GBA Settings
  161. auto* gba_box = new QGroupBox(tr("GBA Settings"), this);
  162. auto* gba_layout = new QGridLayout(gba_box);
  163. gba_box->setLayout(gba_layout);
  164. int gba_row = 0;
  165. m_gba_threads = new QCheckBox(tr("Run GBA Cores in Dedicated Threads"));
  166. gba_layout->addWidget(m_gba_threads, gba_row, 0, 1, -1);
  167. gba_row++;
  168. m_gba_bios_edit = new QLineEdit();
  169. m_gba_browse_bios = new NonDefaultQPushButton(QStringLiteral("..."));
  170. gba_layout->addWidget(new QLabel(tr("BIOS:")), gba_row, 0);
  171. gba_layout->addWidget(m_gba_bios_edit, gba_row, 1);
  172. gba_layout->addWidget(m_gba_browse_bios, gba_row, 2);
  173. gba_row++;
  174. for (size_t i = 0; i < m_gba_rom_edits.size(); ++i)
  175. {
  176. m_gba_rom_edits[i] = new QLineEdit();
  177. m_gba_browse_roms[i] = new NonDefaultQPushButton(QStringLiteral("..."));
  178. gba_layout->addWidget(new QLabel(tr("Port %1 ROM:").arg(i + 1)), gba_row, 0);
  179. gba_layout->addWidget(m_gba_rom_edits[i], gba_row, 1);
  180. gba_layout->addWidget(m_gba_browse_roms[i], gba_row, 2);
  181. gba_row++;
  182. }
  183. m_gba_save_rom_path = new QCheckBox(tr("Save in Same Directory as the ROM"));
  184. gba_layout->addWidget(m_gba_save_rom_path, gba_row, 0, 1, -1);
  185. gba_row++;
  186. m_gba_saves_edit = new QLineEdit();
  187. m_gba_browse_saves = new NonDefaultQPushButton(QStringLiteral("..."));
  188. gba_layout->addWidget(new QLabel(tr("Saves:")), gba_row, 0);
  189. gba_layout->addWidget(m_gba_saves_edit, gba_row, 1);
  190. gba_layout->addWidget(m_gba_browse_saves, gba_row, 2);
  191. gba_row++;
  192. #endif
  193. layout->addWidget(ipl_box);
  194. layout->addWidget(device_box);
  195. #ifdef HAS_LIBMGBA
  196. layout->addWidget(gba_box);
  197. #endif
  198. layout->addStretch();
  199. setLayout(layout);
  200. }
  201. void GameCubePane::ConnectWidgets()
  202. {
  203. // IPL Settings
  204. connect(m_skip_main_menu, &QCheckBox::stateChanged, this, &GameCubePane::SaveSettings);
  205. connect(m_language_combo, &QComboBox::currentIndexChanged, this, &GameCubePane::SaveSettings);
  206. // Device Settings
  207. for (ExpansionInterface::Slot slot : GUI_SLOTS)
  208. {
  209. connect(m_slot_combos[slot], &QComboBox::currentIndexChanged, this,
  210. [this, slot] { UpdateButton(slot); });
  211. connect(m_slot_combos[slot], &QComboBox::currentIndexChanged, this,
  212. &GameCubePane::SaveSettings);
  213. connect(m_slot_buttons[slot], &QPushButton::clicked, [this, slot] { OnConfigPressed(slot); });
  214. }
  215. for (ExpansionInterface::Slot slot : ExpansionInterface::MEMCARD_SLOTS)
  216. {
  217. connect(m_memcard_paths[slot], &QLineEdit::editingFinished, [this, slot] {
  218. // revert path change on failure
  219. if (!SetMemcard(slot, m_memcard_paths[slot]->text()))
  220. LoadSettings();
  221. });
  222. connect(m_agp_paths[slot], &QLineEdit::editingFinished,
  223. [this, slot] { SetAGPRom(slot, m_agp_paths[slot]->text()); });
  224. connect(m_gci_paths[slot], &QLineEdit::editingFinished, [this, slot] {
  225. // revert path change on failure
  226. if (!SetGCIFolder(slot, m_gci_paths[slot]->text()))
  227. LoadSettings();
  228. });
  229. }
  230. #ifdef HAS_LIBMGBA
  231. // GBA Settings
  232. connect(m_gba_threads, &QCheckBox::stateChanged, this, &GameCubePane::SaveSettings);
  233. connect(m_gba_bios_edit, &QLineEdit::editingFinished, this, &GameCubePane::SaveSettings);
  234. connect(m_gba_browse_bios, &QPushButton::clicked, this, &GameCubePane::BrowseGBABios);
  235. connect(m_gba_save_rom_path, &QCheckBox::stateChanged, this, &GameCubePane::SaveRomPathChanged);
  236. connect(m_gba_saves_edit, &QLineEdit::editingFinished, this, &GameCubePane::SaveSettings);
  237. connect(m_gba_browse_saves, &QPushButton::clicked, this, &GameCubePane::BrowseGBASaves);
  238. for (size_t i = 0; i < m_gba_browse_roms.size(); ++i)
  239. {
  240. connect(m_gba_rom_edits[i], &QLineEdit::editingFinished, this, &GameCubePane::SaveSettings);
  241. connect(m_gba_browse_roms[i], &QPushButton::clicked, this, [this, i] { BrowseGBARom(i); });
  242. }
  243. #endif
  244. // Emulation State
  245. connect(&Settings::Instance(), &Settings::EmulationStateChanged, this,
  246. &GameCubePane::OnEmulationStateChanged);
  247. OnEmulationStateChanged();
  248. }
  249. void GameCubePane::OnEmulationStateChanged()
  250. {
  251. #ifdef HAS_LIBMGBA
  252. bool gba_enabled = !NetPlay::IsNetPlayRunning();
  253. m_gba_threads->setEnabled(gba_enabled);
  254. m_gba_bios_edit->setEnabled(gba_enabled);
  255. m_gba_browse_bios->setEnabled(gba_enabled);
  256. m_gba_save_rom_path->setEnabled(gba_enabled);
  257. m_gba_saves_edit->setEnabled(gba_enabled);
  258. m_gba_browse_saves->setEnabled(gba_enabled);
  259. for (size_t i = 0; i < m_gba_browse_roms.size(); ++i)
  260. {
  261. m_gba_rom_edits[i]->setEnabled(gba_enabled);
  262. m_gba_browse_roms[i]->setEnabled(gba_enabled);
  263. }
  264. #endif
  265. }
  266. void GameCubePane::UpdateButton(ExpansionInterface::Slot slot)
  267. {
  268. const auto device =
  269. static_cast<ExpansionInterface::EXIDeviceType>(m_slot_combos[slot]->currentData().toInt());
  270. bool has_config = false;
  271. switch (slot)
  272. {
  273. case ExpansionInterface::Slot::A:
  274. case ExpansionInterface::Slot::B:
  275. {
  276. has_config = (device == ExpansionInterface::EXIDeviceType::MemoryCard ||
  277. device == ExpansionInterface::EXIDeviceType::MemoryCardFolder ||
  278. device == ExpansionInterface::EXIDeviceType::AGP ||
  279. device == ExpansionInterface::EXIDeviceType::Microphone);
  280. const bool hide_memory_card = device != ExpansionInterface::EXIDeviceType::MemoryCard ||
  281. Config::IsDefaultMemcardPathConfigured(slot);
  282. const bool hide_gci_path = device != ExpansionInterface::EXIDeviceType::MemoryCardFolder ||
  283. Config::IsDefaultGCIFolderPathConfigured(slot);
  284. m_memcard_path_labels[slot]->setHidden(hide_memory_card);
  285. m_memcard_paths[slot]->setHidden(hide_memory_card);
  286. m_agp_path_labels[slot]->setHidden(device != ExpansionInterface::EXIDeviceType::AGP);
  287. m_agp_paths[slot]->setHidden(device != ExpansionInterface::EXIDeviceType::AGP);
  288. m_gci_path_labels[slot]->setHidden(hide_gci_path);
  289. m_gci_paths[slot]->setHidden(hide_gci_path);
  290. // In the years before we introduced the GCI folder configuration paths it has become somewhat
  291. // popular to use the GCI override path instead. Check if this is the case and display a warning
  292. // if it is set, so users aren't confused why configuring the normal GCI path doesn't do
  293. // anything.
  294. m_gci_override_labels[slot]->setHidden(
  295. device != ExpansionInterface::EXIDeviceType::MemoryCardFolder ||
  296. Config::Get(Config::GetInfoForGCIPathOverride(slot)).empty());
  297. break;
  298. }
  299. case ExpansionInterface::Slot::SP1:
  300. has_config = (device == ExpansionInterface::EXIDeviceType::Ethernet ||
  301. device == ExpansionInterface::EXIDeviceType::EthernetXLink ||
  302. device == ExpansionInterface::EXIDeviceType::EthernetTapServer ||
  303. device == ExpansionInterface::EXIDeviceType::EthernetBuiltIn ||
  304. device == ExpansionInterface::EXIDeviceType::ModemTapServer);
  305. break;
  306. case ExpansionInterface::Slot::SP2:
  307. has_config = false;
  308. break;
  309. }
  310. m_slot_buttons[slot]->setEnabled(has_config);
  311. }
  312. void GameCubePane::OnConfigPressed(ExpansionInterface::Slot slot)
  313. {
  314. const ExpansionInterface::EXIDeviceType device =
  315. static_cast<ExpansionInterface::EXIDeviceType>(m_slot_combos[slot]->currentData().toInt());
  316. switch (device)
  317. {
  318. case ExpansionInterface::EXIDeviceType::MemoryCard:
  319. BrowseMemcard(slot);
  320. return;
  321. case ExpansionInterface::EXIDeviceType::MemoryCardFolder:
  322. BrowseGCIFolder(slot);
  323. return;
  324. case ExpansionInterface::EXIDeviceType::AGP:
  325. BrowseAGPRom(slot);
  326. return;
  327. case ExpansionInterface::EXIDeviceType::Microphone:
  328. {
  329. // TODO: convert MappingWindow to use Slot?
  330. MappingWindow dialog(this, MappingWindow::Type::MAPPING_GC_MICROPHONE, static_cast<int>(slot));
  331. SetQWidgetWindowDecorations(&dialog);
  332. dialog.exec();
  333. return;
  334. }
  335. case ExpansionInterface::EXIDeviceType::Ethernet:
  336. {
  337. BroadbandAdapterSettingsDialog dialog(this, BroadbandAdapterSettingsDialog::Type::Ethernet);
  338. SetQWidgetWindowDecorations(&dialog);
  339. dialog.exec();
  340. return;
  341. }
  342. case ExpansionInterface::EXIDeviceType::EthernetXLink:
  343. {
  344. BroadbandAdapterSettingsDialog dialog(this, BroadbandAdapterSettingsDialog::Type::XLinkKai);
  345. SetQWidgetWindowDecorations(&dialog);
  346. dialog.exec();
  347. return;
  348. }
  349. case ExpansionInterface::EXIDeviceType::EthernetTapServer:
  350. {
  351. BroadbandAdapterSettingsDialog dialog(this, BroadbandAdapterSettingsDialog::Type::TapServer);
  352. SetQWidgetWindowDecorations(&dialog);
  353. dialog.exec();
  354. return;
  355. }
  356. case ExpansionInterface::EXIDeviceType::ModemTapServer:
  357. {
  358. BroadbandAdapterSettingsDialog dialog(this,
  359. BroadbandAdapterSettingsDialog::Type::ModemTapServer);
  360. SetQWidgetWindowDecorations(&dialog);
  361. dialog.exec();
  362. return;
  363. }
  364. case ExpansionInterface::EXIDeviceType::EthernetBuiltIn:
  365. {
  366. BroadbandAdapterSettingsDialog dialog(this, BroadbandAdapterSettingsDialog::Type::BuiltIn);
  367. SetQWidgetWindowDecorations(&dialog);
  368. dialog.exec();
  369. return;
  370. }
  371. default:
  372. PanicAlertFmt("Unknown settings pressed for {}", device);
  373. return;
  374. }
  375. }
  376. void GameCubePane::BrowseMemcard(ExpansionInterface::Slot slot)
  377. {
  378. ASSERT(ExpansionInterface::IsMemcardSlot(slot));
  379. const QString filename = DolphinFileDialog::getSaveFileName(
  380. this, tr("Choose a File to Open or Create"),
  381. QString::fromStdString(File::GetUserPath(D_GCUSER_IDX)),
  382. tr("GameCube Memory Cards (*.raw *.gcp)"), nullptr, QFileDialog::DontConfirmOverwrite);
  383. if (!filename.isEmpty())
  384. SetMemcard(slot, filename);
  385. }
  386. bool GameCubePane::SetMemcard(ExpansionInterface::Slot slot, const QString& filename)
  387. {
  388. if (filename.isEmpty())
  389. {
  390. ModalMessageBox::critical(this, tr("Error"), tr("Cannot set memory card to an empty path."));
  391. return false;
  392. }
  393. const std::string raw_path =
  394. WithUnifiedPathSeparators(QFileInfo(filename).absoluteFilePath().toStdString());
  395. // Figure out if the user selected a card that has a valid region specifier in the filename.
  396. const std::string jp_path = Config::GetMemcardPath(raw_path, slot, DiscIO::Region::NTSC_J);
  397. const std::string us_path = Config::GetMemcardPath(raw_path, slot, DiscIO::Region::NTSC_U);
  398. const std::string eu_path = Config::GetMemcardPath(raw_path, slot, DiscIO::Region::PAL);
  399. const bool raw_path_valid = raw_path == jp_path || raw_path == us_path || raw_path == eu_path;
  400. if (!raw_path_valid)
  401. {
  402. // TODO: We could try to autodetect the card region here and offer automatic renaming.
  403. ModalMessageBox::critical(this, tr("Error"),
  404. tr("The filename %1 does not conform to Dolphin's region code format "
  405. "for memory cards. Please rename this file to either %2, %3, or "
  406. "%4, matching the region of the save files that are on it.")
  407. .arg(QString::fromStdString(PathToFileName(raw_path)))
  408. .arg(QString::fromStdString(PathToFileName(us_path)))
  409. .arg(QString::fromStdString(PathToFileName(eu_path)))
  410. .arg(QString::fromStdString(PathToFileName(jp_path))));
  411. return false;
  412. }
  413. // Memcard validity checks
  414. for (const std::string& path : {jp_path, us_path, eu_path})
  415. {
  416. if (File::Exists(path))
  417. {
  418. auto [error_code, mc] = Memcard::GCMemcard::Open(path);
  419. if (error_code.HasCriticalErrors() || !mc || !mc->IsValid())
  420. {
  421. ModalMessageBox::critical(
  422. this, tr("Error"),
  423. tr("The file\n%1\nis either corrupted or not a GameCube memory card file.\n%2")
  424. .arg(QString::fromStdString(path))
  425. .arg(GCMemcardManager::GetErrorMessagesForErrorCode(error_code)));
  426. return false;
  427. }
  428. }
  429. }
  430. // Check if the other slot has the same memory card configured and refuse to use this card if so.
  431. // The EU path is compared here, but it doesn't actually matter which one we compare since they
  432. // follow a known pattern, so if the EU path matches the other match too and vice-versa.
  433. for (ExpansionInterface::Slot other_slot : ExpansionInterface::MEMCARD_SLOTS)
  434. {
  435. if (other_slot == slot)
  436. continue;
  437. const std::string other_eu_path = Config::GetMemcardPath(other_slot, DiscIO::Region::PAL);
  438. if (eu_path == other_eu_path)
  439. {
  440. ModalMessageBox::critical(
  441. this, tr("Error"),
  442. tr("The same file can't be used in multiple slots; it is already used by %1.")
  443. .arg(QString::fromStdString(fmt::to_string(other_slot))));
  444. return false;
  445. }
  446. }
  447. const std::string old_eu_path = Config::GetMemcardPath(slot, DiscIO::Region::PAL);
  448. Config::SetBase(Config::GetInfoForMemcardPath(slot), raw_path);
  449. auto& system = Core::System::GetInstance();
  450. if (Core::IsRunning(system))
  451. {
  452. // If emulation is running and the new card is different from the old one, notify the system to
  453. // eject the old and insert the new card.
  454. // TODO: This should probably done by a config change callback instead.
  455. if (eu_path != old_eu_path)
  456. {
  457. // ChangeDevice unplugs the device for 1 second, which means that games should notice that
  458. // the path has changed and thus the memory card contents have changed
  459. system.GetExpansionInterface().ChangeDevice(slot,
  460. ExpansionInterface::EXIDeviceType::MemoryCard);
  461. }
  462. }
  463. LoadSettings();
  464. return true;
  465. }
  466. void GameCubePane::BrowseGCIFolder(ExpansionInterface::Slot slot)
  467. {
  468. ASSERT(ExpansionInterface::IsMemcardSlot(slot));
  469. const QString path = DolphinFileDialog::getExistingDirectory(
  470. this, tr("Choose GCI Base Folder"), QString::fromStdString(File::GetUserPath(D_GCUSER_IDX)));
  471. if (!path.isEmpty())
  472. SetGCIFolder(slot, path);
  473. }
  474. bool GameCubePane::SetGCIFolder(ExpansionInterface::Slot slot, const QString& path)
  475. {
  476. if (path.isEmpty())
  477. {
  478. ModalMessageBox::critical(this, tr("Error"), tr("Cannot set GCI folder to an empty path."));
  479. return false;
  480. }
  481. std::string raw_path =
  482. WithUnifiedPathSeparators(QFileInfo(path).absoluteFilePath().toStdString());
  483. while (raw_path.ends_with('/'))
  484. raw_path.pop_back();
  485. // The user might be attempting to reset this path to its default, check for this.
  486. const std::string default_jp_path = Config::GetGCIFolderPath("", slot, DiscIO::Region::NTSC_J);
  487. const std::string default_us_path = Config::GetGCIFolderPath("", slot, DiscIO::Region::NTSC_U);
  488. const std::string default_eu_path = Config::GetGCIFolderPath("", slot, DiscIO::Region::PAL);
  489. const bool is_default_path =
  490. raw_path == default_jp_path || raw_path == default_us_path || raw_path == default_eu_path;
  491. bool path_changed;
  492. if (is_default_path)
  493. {
  494. // Reset to default.
  495. // Note that this does not need to check if the same card is in the other slot, because that's
  496. // impossible given our constraints for folder names.
  497. raw_path = "";
  498. path_changed = !Config::IsDefaultGCIFolderPathConfigured(slot);
  499. }
  500. else
  501. {
  502. // Figure out if the user selected a folder that ends in a valid region specifier.
  503. const std::string jp_path = Config::GetGCIFolderPath(raw_path, slot, DiscIO::Region::NTSC_J);
  504. const std::string us_path = Config::GetGCIFolderPath(raw_path, slot, DiscIO::Region::NTSC_U);
  505. const std::string eu_path = Config::GetGCIFolderPath(raw_path, slot, DiscIO::Region::PAL);
  506. const bool raw_path_valid = raw_path == jp_path || raw_path == us_path || raw_path == eu_path;
  507. if (!raw_path_valid)
  508. {
  509. // TODO: We could try to autodetect the card region here and offer automatic renaming.
  510. ModalMessageBox::critical(
  511. this, tr("Error"),
  512. tr("The folder %1 does not conform to Dolphin's region code format "
  513. "for GCI folders. Please rename this folder to either %2, %3, or "
  514. "%4, matching the region of the save files that are in it.")
  515. .arg(QString::fromStdString(PathToFileName(raw_path)))
  516. .arg(QString::fromStdString(PathToFileName(us_path)))
  517. .arg(QString::fromStdString(PathToFileName(eu_path)))
  518. .arg(QString::fromStdString(PathToFileName(jp_path))));
  519. return false;
  520. }
  521. // Check if the other slot has the same folder configured and refuse to use this folder if so.
  522. // The EU path is compared here, but it doesn't actually matter which one we compare since they
  523. // follow a known pattern, so if the EU path matches the other match too and vice-versa.
  524. for (ExpansionInterface::Slot other_slot : ExpansionInterface::MEMCARD_SLOTS)
  525. {
  526. if (other_slot == slot)
  527. continue;
  528. const std::string other_eu_path = Config::GetGCIFolderPath(other_slot, DiscIO::Region::PAL);
  529. if (eu_path == other_eu_path)
  530. {
  531. ModalMessageBox::critical(
  532. this, tr("Error"),
  533. tr("The same folder can't be used in multiple slots; it is already used by %1.")
  534. .arg(QString::fromStdString(fmt::to_string(other_slot))));
  535. return false;
  536. }
  537. }
  538. path_changed = eu_path != Config::GetGCIFolderPath(slot, DiscIO::Region::PAL);
  539. }
  540. Config::SetBase(Config::GetInfoForGCIPath(slot), raw_path);
  541. auto& system = Core::System::GetInstance();
  542. if (Core::IsRunning(system))
  543. {
  544. // If emulation is running and the new card is different from the old one, notify the system to
  545. // eject the old and insert the new card.
  546. // TODO: This should probably be done by a config change callback instead.
  547. if (path_changed)
  548. {
  549. // ChangeDevice unplugs the device for 1 second, which means that games should notice that
  550. // the path has changed and thus the memory card contents have changed
  551. system.GetExpansionInterface().ChangeDevice(
  552. slot, ExpansionInterface::EXIDeviceType::MemoryCardFolder);
  553. }
  554. }
  555. LoadSettings();
  556. return true;
  557. }
  558. void GameCubePane::BrowseAGPRom(ExpansionInterface::Slot slot)
  559. {
  560. ASSERT(ExpansionInterface::IsMemcardSlot(slot));
  561. QString filename = DolphinFileDialog::getSaveFileName(
  562. this, tr("Choose a File to Open"), QString::fromStdString(File::GetUserPath(D_GCUSER_IDX)),
  563. tr("Game Boy Advance Carts (*.gba)"), nullptr, QFileDialog::DontConfirmOverwrite);
  564. if (!filename.isEmpty())
  565. SetAGPRom(slot, filename);
  566. }
  567. void GameCubePane::SetAGPRom(ExpansionInterface::Slot slot, const QString& filename)
  568. {
  569. QString path_abs = filename.isEmpty() ? QString() : QFileInfo(filename).absoluteFilePath();
  570. QString path_old =
  571. QFileInfo(QString::fromStdString(Config::Get(Config::GetInfoForAGPCartPath(slot))))
  572. .absoluteFilePath();
  573. Config::SetBase(Config::GetInfoForAGPCartPath(slot), path_abs.toStdString());
  574. auto& system = Core::System::GetInstance();
  575. if (Core::IsRunning(system) && path_abs != path_old)
  576. {
  577. // ChangeDevice unplugs the device for 1 second. For an actual AGP, you can remove the
  578. // cartridge without unplugging it, and it's not clear if the AGP software actually notices
  579. // that it's been unplugged or the cartridge has changed, but this was done for memcards so
  580. // we might as well do it for the AGP too.
  581. system.GetExpansionInterface().ChangeDevice(slot, ExpansionInterface::EXIDeviceType::AGP);
  582. }
  583. LoadSettings();
  584. }
  585. void GameCubePane::BrowseGBABios()
  586. {
  587. QString file = QDir::toNativeSeparators(DolphinFileDialog::getOpenFileName(
  588. this, tr("Select GBA BIOS"), QString::fromStdString(File::GetUserPath(F_GBABIOS_IDX)),
  589. tr("All Files (*)")));
  590. if (!file.isEmpty())
  591. {
  592. m_gba_bios_edit->setText(file);
  593. SaveSettings();
  594. }
  595. }
  596. void GameCubePane::BrowseGBARom(size_t index)
  597. {
  598. QString file = QString::fromStdString(GetOpenGBARom({}));
  599. if (!file.isEmpty())
  600. {
  601. m_gba_rom_edits[index]->setText(file);
  602. SaveSettings();
  603. }
  604. }
  605. void GameCubePane::SaveRomPathChanged()
  606. {
  607. m_gba_saves_edit->setEnabled(!m_gba_save_rom_path->isChecked());
  608. m_gba_browse_saves->setEnabled(!m_gba_save_rom_path->isChecked());
  609. SaveSettings();
  610. }
  611. void GameCubePane::BrowseGBASaves()
  612. {
  613. QString dir = QDir::toNativeSeparators(DolphinFileDialog::getExistingDirectory(
  614. this, tr("Select GBA Saves Path"),
  615. QString::fromStdString(File::GetUserPath(D_GBASAVES_IDX))));
  616. if (!dir.isEmpty())
  617. {
  618. m_gba_saves_edit->setText(dir);
  619. SaveSettings();
  620. }
  621. }
  622. void GameCubePane::LoadSettings()
  623. {
  624. // IPL Settings
  625. SignalBlocking(m_skip_main_menu)->setChecked(Config::Get(Config::MAIN_SKIP_IPL));
  626. SignalBlocking(m_language_combo)
  627. ->setCurrentIndex(m_language_combo->findData(Config::Get(Config::MAIN_GC_LANGUAGE)));
  628. bool have_menu = false;
  629. for (const std::string dir : {USA_DIR, JAP_DIR, EUR_DIR})
  630. {
  631. const auto path = DIR_SEP + dir + DIR_SEP GC_IPL;
  632. if (File::Exists(File::GetUserPath(D_GCUSER_IDX) + path) ||
  633. File::Exists(File::GetSysDirectory() + GC_SYS_DIR + path))
  634. {
  635. have_menu = true;
  636. break;
  637. }
  638. }
  639. m_skip_main_menu->setEnabled(have_menu);
  640. m_skip_main_menu->setToolTip(have_menu ? QString{} : tr("Put IPL ROMs in User/GC/<region>."));
  641. // Device Settings
  642. for (ExpansionInterface::Slot slot : GUI_SLOTS)
  643. {
  644. const ExpansionInterface::EXIDeviceType exi_device =
  645. Config::Get(Config::GetInfoForEXIDevice(slot));
  646. SignalBlocking(m_slot_combos[slot])
  647. ->setCurrentIndex(m_slot_combos[slot]->findData(static_cast<int>(exi_device)));
  648. UpdateButton(slot);
  649. }
  650. for (ExpansionInterface::Slot slot : ExpansionInterface::MEMCARD_SLOTS)
  651. {
  652. SignalBlocking(m_memcard_paths[slot])
  653. ->setText(QString::fromStdString(Config::GetMemcardPath(slot, std::nullopt)));
  654. SignalBlocking(m_agp_paths[slot])
  655. ->setText(QString::fromStdString(Config::Get(Config::GetInfoForAGPCartPath(slot))));
  656. SignalBlocking(m_gci_paths[slot])
  657. ->setText(QString::fromStdString(Config::GetGCIFolderPath(slot, std::nullopt)));
  658. }
  659. #ifdef HAS_LIBMGBA
  660. // GBA Settings
  661. SignalBlocking(m_gba_threads)->setChecked(Config::Get(Config::MAIN_GBA_THREADS));
  662. SignalBlocking(m_gba_bios_edit)
  663. ->setText(QString::fromStdString(File::GetUserPath(F_GBABIOS_IDX)));
  664. SignalBlocking(m_gba_save_rom_path)->setChecked(Config::Get(Config::MAIN_GBA_SAVES_IN_ROM_PATH));
  665. SignalBlocking(m_gba_saves_edit)
  666. ->setText(QString::fromStdString(File::GetUserPath(D_GBASAVES_IDX)));
  667. for (size_t i = 0; i < m_gba_rom_edits.size(); ++i)
  668. {
  669. SignalBlocking(m_gba_rom_edits[i])
  670. ->setText(QString::fromStdString(Config::Get(Config::MAIN_GBA_ROM_PATHS[i])));
  671. }
  672. #endif
  673. }
  674. void GameCubePane::SaveSettings()
  675. {
  676. Config::ConfigChangeCallbackGuard config_guard;
  677. // IPL Settings
  678. Config::SetBaseOrCurrent(Config::MAIN_SKIP_IPL, m_skip_main_menu->isChecked());
  679. Config::SetBaseOrCurrent(Config::MAIN_GC_LANGUAGE, m_language_combo->currentData().toInt());
  680. auto& system = Core::System::GetInstance();
  681. // Device Settings
  682. for (ExpansionInterface::Slot slot : GUI_SLOTS)
  683. {
  684. const auto dev =
  685. static_cast<ExpansionInterface::EXIDeviceType>(m_slot_combos[slot]->currentData().toInt());
  686. const ExpansionInterface::EXIDeviceType current_exi_device =
  687. Config::Get(Config::GetInfoForEXIDevice(slot));
  688. if (Core::IsRunning(system) && current_exi_device != dev)
  689. {
  690. system.GetExpansionInterface().ChangeDevice(slot, dev);
  691. }
  692. Config::SetBaseOrCurrent(Config::GetInfoForEXIDevice(slot), dev);
  693. }
  694. #ifdef HAS_LIBMGBA
  695. // GBA Settings
  696. if (!NetPlay::IsNetPlayRunning())
  697. {
  698. Config::SetBaseOrCurrent(Config::MAIN_GBA_THREADS, m_gba_threads->isChecked());
  699. Config::SetBaseOrCurrent(Config::MAIN_GBA_BIOS_PATH, m_gba_bios_edit->text().toStdString());
  700. Config::SetBaseOrCurrent(Config::MAIN_GBA_SAVES_IN_ROM_PATH, m_gba_save_rom_path->isChecked());
  701. Config::SetBaseOrCurrent(Config::MAIN_GBA_SAVES_PATH, m_gba_saves_edit->text().toStdString());
  702. File::SetUserPath(F_GBABIOS_IDX, Config::Get(Config::MAIN_GBA_BIOS_PATH));
  703. File::SetUserPath(D_GBASAVES_IDX, Config::Get(Config::MAIN_GBA_SAVES_PATH));
  704. for (size_t i = 0; i < m_gba_rom_edits.size(); ++i)
  705. {
  706. Config::SetBaseOrCurrent(Config::MAIN_GBA_ROM_PATHS[i],
  707. m_gba_rom_edits[i]->text().toStdString());
  708. }
  709. auto server = Settings::Instance().GetNetPlayServer();
  710. if (server)
  711. server->SetGBAConfig(server->GetGBAConfig(), true);
  712. }
  713. #endif
  714. LoadSettings();
  715. }
  716. std::string GameCubePane::GetOpenGBARom(std::string_view title)
  717. {
  718. QString caption = tr("Select GBA ROM");
  719. if (!title.empty())
  720. caption += QStringLiteral(": %1").arg(QString::fromStdString(std::string(title)));
  721. return QDir::toNativeSeparators(
  722. DolphinFileDialog::getOpenFileName(
  723. nullptr, caption, QString(),
  724. tr("Game Boy Advance ROMs (*.gba *.gbc *.gb *.7z *.zip *.agb *.mb *.rom *.bin);;"
  725. "All Files (*)")))
  726. .toStdString();
  727. }