diff --git a/Source/Core/Core/Config/MainSettings.cpp b/Source/Core/Core/Config/MainSettings.cpp index 08b3d586f1..091fd3504f 100644 --- a/Source/Core/Core/Config/MainSettings.cpp +++ b/Source/Core/Core/Config/MainSettings.cpp @@ -76,6 +76,18 @@ const Info& GetInfoForAGPCartPath(ExpansionInterface::Slot slot) }; return *infos[slot]; } +const Info MAIN_GCI_FOLDER_A_PATH{{System::Main, "Core", "GCIFolderAPath"}, ""}; +const Info MAIN_GCI_FOLDER_B_PATH{{System::Main, "Core", "GCIFolderBPath"}, ""}; +const Info& GetInfoForGCIPath(ExpansionInterface::Slot slot) +{ + ASSERT(ExpansionInterface::IsMemcardSlot(slot)); + static constexpr Common::EnumMap*, ExpansionInterface::MAX_MEMCARD_SLOT> + infos{ + &MAIN_GCI_FOLDER_A_PATH, + &MAIN_GCI_FOLDER_B_PATH, + }; + return *infos[slot]; +} const Info MAIN_GCI_FOLDER_A_PATH_OVERRIDE{ {System::Main, "Core", "GCIFolderAPathOverride"}, ""}; const Info MAIN_GCI_FOLDER_B_PATH_OVERRIDE{ @@ -650,4 +662,63 @@ bool IsDefaultMemcardPathConfigured(ExpansionInterface::Slot slot) { return Config::Get(GetInfoForMemcardPath(slot)).empty(); } + +std::string GetGCIFolderPath(ExpansionInterface::Slot slot, std::optional region) +{ + return GetGCIFolderPath(Config::Get(GetInfoForGCIPath(slot)), slot, region); +} + +std::string GetGCIFolderPath(std::string configured_folder, ExpansionInterface::Slot slot, + std::optional region) +{ + if (configured_folder.empty()) + { + const auto region_dir = Config::GetDirectoryForRegion( + Config::ToGameCubeRegion(region ? *region : Config::Get(Config::MAIN_FALLBACK_REGION))); + const bool is_slot_a = slot == ExpansionInterface::Slot::A; + return fmt::format("{}{}/Card {}", File::GetUserPath(D_GCUSER_IDX), region_dir, + is_slot_a ? 'A' : 'B'); + } + + // Custom path is expected to be stored in the form of + // "/path/to/folder/{region_code}" + // with an arbitrary but supported region code. + // Try to extract and replace that region code. + // If there's no region code just insert one at the end. + + UnifyPathSeparators(configured_folder); + while (StringEndsWith(configured_folder, "/")) + configured_folder.pop_back(); + + constexpr std::string_view us_region = "/" USA_DIR; + constexpr std::string_view jp_region = "/" JAP_DIR; + constexpr std::string_view eu_region = "/" EUR_DIR; + std::string_view base_path = configured_folder; + std::optional path_region = std::nullopt; + if (StringEndsWith(base_path, us_region)) + { + base_path = base_path.substr(0, base_path.size() - us_region.size()); + path_region = DiscIO::Region::NTSC_U; + } + else if (StringEndsWith(base_path, jp_region)) + { + base_path = base_path.substr(0, base_path.size() - jp_region.size()); + path_region = DiscIO::Region::NTSC_J; + } + else if (StringEndsWith(base_path, eu_region)) + { + base_path = base_path.substr(0, base_path.size() - eu_region.size()); + path_region = DiscIO::Region::PAL; + } + + const DiscIO::Region used_region = + region ? *region : (path_region ? *path_region : Config::Get(Config::MAIN_FALLBACK_REGION)); + return fmt::format("{}/{}", base_path, + Config::GetDirectoryForRegion(Config::ToGameCubeRegion(used_region))); +} + +bool IsDefaultGCIFolderPathConfigured(ExpansionInterface::Slot slot) +{ + return Config::Get(GetInfoForGCIPath(slot)).empty(); +} } // namespace Config diff --git a/Source/Core/Core/Config/MainSettings.h b/Source/Core/Core/Config/MainSettings.h index 4aab4dac1c..f30ef02a38 100644 --- a/Source/Core/Core/Config/MainSettings.h +++ b/Source/Core/Core/Config/MainSettings.h @@ -75,6 +75,9 @@ const Info& GetInfoForMemcardPath(ExpansionInterface::Slot slot); extern const Info MAIN_AGP_CART_A_PATH; extern const Info MAIN_AGP_CART_B_PATH; const Info& GetInfoForAGPCartPath(ExpansionInterface::Slot slot); +extern const Info MAIN_GCI_FOLDER_A_PATH; +extern const Info MAIN_GCI_FOLDER_B_PATH; +const Info& GetInfoForGCIPath(ExpansionInterface::Slot slot); extern const Info MAIN_GCI_FOLDER_A_PATH_OVERRIDE; extern const Info MAIN_GCI_FOLDER_B_PATH_OVERRIDE; const Info& GetInfoForGCIPathOverride(ExpansionInterface::Slot slot); @@ -352,4 +355,8 @@ std::string GetMemcardPath(ExpansionInterface::Slot slot, std::optional region, u16 size_mb = 0x80); bool IsDefaultMemcardPathConfigured(ExpansionInterface::Slot slot); +std::string GetGCIFolderPath(ExpansionInterface::Slot slot, std::optional region); +std::string GetGCIFolderPath(std::string configured_folder, ExpansionInterface::Slot slot, + std::optional region); +bool IsDefaultGCIFolderPathConfigured(ExpansionInterface::Slot slot); } // namespace Config diff --git a/Source/Core/Core/ConfigLoaders/IsSettingSaveable.cpp b/Source/Core/Core/ConfigLoaders/IsSettingSaveable.cpp index 97fda95b3b..bd7f28fe3c 100644 --- a/Source/Core/Core/ConfigLoaders/IsSettingSaveable.cpp +++ b/Source/Core/Core/ConfigLoaders/IsSettingSaveable.cpp @@ -131,6 +131,8 @@ bool IsSettingSaveable(const Config::Location& config_location) &Config::MAIN_SYNC_GPU_MIN_DISTANCE.GetLocation(), &Config::MAIN_SYNC_GPU_OVERCLOCK.GetLocation(), &Config::MAIN_OVERRIDE_BOOT_IOS.GetLocation(), + &Config::MAIN_GCI_FOLDER_A_PATH.GetLocation(), + &Config::MAIN_GCI_FOLDER_B_PATH.GetLocation(), // UI.General diff --git a/Source/Core/Core/HW/EXI/EXI_DeviceMemoryCard.cpp b/Source/Core/Core/HW/EXI/EXI_DeviceMemoryCard.cpp index 2aa5c1bbb5..c054c47404 100644 --- a/Source/Core/Core/HW/EXI/EXI_DeviceMemoryCard.cpp +++ b/Source/Core/Core/HW/EXI/EXI_DeviceMemoryCard.cpp @@ -154,20 +154,20 @@ CEXIMemoryCard::GetGCIFolderPath(Slot card_slot, AllowMovieFolder allow_movie_fo if (!path_override.empty()) return {std::move(path_override), false}; - std::string path = File::GetUserPath(D_GCUSER_IDX); - const bool use_movie_folder = allow_movie_folder == AllowMovieFolder::Yes && Movie::IsPlayingInput() && Movie::IsConfigSaved() && Movie::IsUsingMemcard(card_slot) && Movie::IsStartingFromClearSave(); - if (use_movie_folder) - path += "Movie" DIR_SEP; - const DiscIO::Region region = Config::ToGameCubeRegion(SConfig::GetInstance().m_region); - path = path + Config::GetDirectoryForRegion(region) + DIR_SEP + - fmt::format("Card {}", s_card_short_names[card_slot]); - return {std::move(path), !use_movie_folder}; + if (use_movie_folder) + { + return {fmt::format("{}{}/Movie/Card {}", File::GetUserPath(D_GCUSER_IDX), + Config::GetDirectoryForRegion(region), s_card_short_names[card_slot]), + false}; + } + + return {Config::GetGCIFolderPath(card_slot, region), true}; } void CEXIMemoryCard::SetupGciFolder(const Memcard::HeaderData& header_data) diff --git a/Source/Core/Core/NetPlayServer.cpp b/Source/Core/Core/NetPlayServer.cpp index 18076ada29..ed2110ad16 100644 --- a/Source/Core/Core/NetPlayServer.cpp +++ b/Source/Core/Core/NetPlayServer.cpp @@ -1686,7 +1686,8 @@ bool NetPlayServer::SyncSaveData(const SaveSyncInfo& sync_info) return true; const auto game_region = sync_info.game->GetRegion(); - const std::string region = Config::GetDirectoryForRegion(Config::ToGameCubeRegion(game_region)); + const auto gamecube_region = Config::ToGameCubeRegion(game_region); + const std::string region = Config::GetDirectoryForRegion(gamecube_region); for (ExpansionInterface::Slot slot : ExpansionInterface::MEMCARD_SLOTS) { @@ -1723,8 +1724,7 @@ bool NetPlayServer::SyncSaveData(const SaveSyncInfo& sync_info) else if (Config::Get(Config::GetInfoForEXIDevice(slot)) == ExpansionInterface::EXIDeviceType::MemoryCardFolder) { - const std::string path = File::GetUserPath(D_GCUSER_IDX) + region + DIR_SEP + - fmt::format("Card {}", is_slot_a ? 'A' : 'B'); + const std::string path = Config::GetGCIFolderPath(slot, gamecube_region); sf::Packet pac; pac << MessageID::SyncSaveData; diff --git a/Source/Core/DolphinQt/GameList/GameList.cpp b/Source/Core/DolphinQt/GameList/GameList.cpp index db86079825..69afc13a74 100644 --- a/Source/Core/DolphinQt/GameList/GameList.cpp +++ b/Source/Core/DolphinQt/GameList/GameList.cpp @@ -716,16 +716,10 @@ void GameList::OpenGCSaveFolder() { case ExpansionInterface::EXIDeviceType::MemoryCardFolder: { - std::string path = fmt::format("{}/{}/{}", File::GetUserPath(D_GCUSER_IDX), - Config::GetDirectoryForRegion(game->GetRegion()), - slot == Slot::A ? "Card A" : "Card B"); - std::string override_path = Config::Get(Config::GetInfoForGCIPathOverride(slot)); - - if (!override_path.empty()) - path = override_path; - - QDir dir(QString::fromStdString(path)); + QDir dir(QString::fromStdString(override_path.empty() ? + Config::GetGCIFolderPath(slot, game->GetRegion()) : + override_path)); if (!dir.entryList({QStringLiteral("%1-%2-*.gci") .arg(QString::fromStdString(game->GetMakerID())) diff --git a/Source/Core/DolphinQt/Settings/GameCubePane.cpp b/Source/Core/DolphinQt/Settings/GameCubePane.cpp index cc2b8fbe5f..b4e02c8305 100644 --- a/Source/Core/DolphinQt/Settings/GameCubePane.cpp +++ b/Source/Core/DolphinQt/Settings/GameCubePane.cpp @@ -114,6 +114,12 @@ void GameCubePane::CreateWidgets() m_agp_paths[slot] = new QLineEdit(); m_agp_path_layouts[slot]->addWidget(m_agp_path_labels[slot]); m_agp_path_layouts[slot]->addWidget(m_agp_paths[slot]); + + m_gci_path_layouts[slot] = new QHBoxLayout(); + m_gci_path_labels[slot] = new QLabel(tr("GCI Folder Path:")); + m_gci_paths[slot] = new QLineEdit(); + m_gci_path_layouts[slot]->addWidget(m_gci_path_labels[slot]); + m_gci_path_layouts[slot]->addWidget(m_gci_paths[slot]); } // Add slot devices @@ -155,6 +161,9 @@ void GameCubePane::CreateWidgets() ++row; device_layout->addLayout(m_agp_path_layouts[ExpansionInterface::Slot::A], row, 0, 1, 3); + ++row; + device_layout->addLayout(m_gci_path_layouts[ExpansionInterface::Slot::A], row, 0, 1, 3); + ++row; device_layout->addWidget(new QLabel(tr("Slot B:")), row, 0); device_layout->addWidget(m_slot_combos[ExpansionInterface::Slot::B], row, 1); @@ -166,6 +175,9 @@ void GameCubePane::CreateWidgets() ++row; device_layout->addLayout(m_agp_path_layouts[ExpansionInterface::Slot::B], row, 0, 1, 3); + ++row; + device_layout->addLayout(m_gci_path_layouts[ExpansionInterface::Slot::B], row, 0, 1, 3); + ++row; device_layout->addWidget(new QLabel(tr("SP1:")), row, 0); device_layout->addWidget(m_slot_combos[ExpansionInterface::Slot::SP1], row, 1); @@ -249,6 +261,11 @@ void GameCubePane::ConnectWidgets() }); connect(m_agp_paths[slot], &QLineEdit::editingFinished, [this, slot] { SetAGPRom(slot, m_agp_paths[slot]->text()); }); + connect(m_gci_paths[slot], &QLineEdit::editingFinished, [this, slot] { + // revert path change on failure + if (!SetGCIFolder(slot, m_gci_paths[slot]->text())) + LoadSettings(); + }); } #ifdef HAS_LIBMGBA @@ -302,14 +319,19 @@ void GameCubePane::UpdateButton(ExpansionInterface::Slot slot) case ExpansionInterface::Slot::B: { has_config = (device == ExpansionInterface::EXIDeviceType::MemoryCard || + device == ExpansionInterface::EXIDeviceType::MemoryCardFolder || device == ExpansionInterface::EXIDeviceType::AGP || device == ExpansionInterface::EXIDeviceType::Microphone); const bool hide_memory_card = device != ExpansionInterface::EXIDeviceType::MemoryCard || Config::IsDefaultMemcardPathConfigured(slot); + const bool hide_gci_path = device != ExpansionInterface::EXIDeviceType::MemoryCardFolder || + Config::IsDefaultGCIFolderPathConfigured(slot); m_memcard_path_labels[slot]->setHidden(hide_memory_card); m_memcard_paths[slot]->setHidden(hide_memory_card); m_agp_path_labels[slot]->setHidden(device != ExpansionInterface::EXIDeviceType::AGP); m_agp_paths[slot]->setHidden(device != ExpansionInterface::EXIDeviceType::AGP); + m_gci_path_labels[slot]->setHidden(hide_gci_path); + m_gci_paths[slot]->setHidden(hide_gci_path); break; } case ExpansionInterface::Slot::SP1: @@ -332,6 +354,9 @@ void GameCubePane::OnConfigPressed(ExpansionInterface::Slot slot) case ExpansionInterface::EXIDeviceType::MemoryCard: BrowseMemcard(slot); return; + case ExpansionInterface::EXIDeviceType::MemoryCardFolder: + BrowseGCIFolder(slot); + return; case ExpansionInterface::EXIDeviceType::AGP: BrowseAGPRom(slot); return; @@ -462,6 +487,111 @@ bool GameCubePane::SetMemcard(ExpansionInterface::Slot slot, const QString& file return true; } +void GameCubePane::BrowseGCIFolder(ExpansionInterface::Slot slot) +{ + ASSERT(ExpansionInterface::IsMemcardSlot(slot)); + + const QString path = DolphinFileDialog::getExistingDirectory( + this, tr("Choose the GCI base folder"), + QString::fromStdString(File::GetUserPath(D_GCUSER_IDX))); + + if (!path.isEmpty()) + SetGCIFolder(slot, path); +} + +bool GameCubePane::SetGCIFolder(ExpansionInterface::Slot slot, const QString& path) +{ + if (path.isEmpty()) + { + ModalMessageBox::critical(this, tr("Error"), tr("Cannot set GCI folder to an empty path.")); + return false; + } + + std::string raw_path = + WithUnifiedPathSeparators(QFileInfo(path).absoluteFilePath().toStdString()); + while (StringEndsWith(raw_path, "/")) + raw_path.pop_back(); + + // The user might be attempting to reset this path to its default, check for this. + const std::string default_jp_path = Config::GetGCIFolderPath("", slot, DiscIO::Region::NTSC_J); + const std::string default_us_path = Config::GetGCIFolderPath("", slot, DiscIO::Region::NTSC_U); + const std::string default_eu_path = Config::GetGCIFolderPath("", slot, DiscIO::Region::PAL); + const bool is_default_path = + raw_path == default_jp_path || raw_path == default_us_path || raw_path == default_eu_path; + + bool path_changed; + if (is_default_path) + { + // Reset to default. + // Note that this does not need to check if the same card is in the other slot, because that's + // impossible given our constraints for folder names. + raw_path = ""; + path_changed = !Config::IsDefaultGCIFolderPathConfigured(slot); + } + else + { + // Figure out if the user selected a folder that ends in a valid region specifier. + const std::string jp_path = Config::GetGCIFolderPath(raw_path, slot, DiscIO::Region::NTSC_J); + const std::string us_path = Config::GetGCIFolderPath(raw_path, slot, DiscIO::Region::NTSC_U); + const std::string eu_path = Config::GetGCIFolderPath(raw_path, slot, DiscIO::Region::PAL); + const bool raw_path_valid = raw_path == jp_path || raw_path == us_path || raw_path == eu_path; + + if (!raw_path_valid) + { + // TODO: We could try to autodetect the card region here and offer automatic renaming. + ModalMessageBox::critical( + this, tr("Error"), + tr("The folder %1 does not conform to Dolphin's region code format " + "for GCI folders. Please rename this folder to either %2, %3, or " + "%4, matching the region of the save files that are in it.") + .arg(QString::fromStdString(PathToFileName(raw_path))) + .arg(QString::fromStdString(PathToFileName(us_path))) + .arg(QString::fromStdString(PathToFileName(eu_path))) + .arg(QString::fromStdString(PathToFileName(jp_path)))); + return false; + } + + // Check if the other slot has the same folder configured and refuse to use this folder if so. + // The EU path is compared here, but it doesn't actually matter which one we compare since they + // follow a known pattern, so if the EU path matches the other match too and vice-versa. + for (ExpansionInterface::Slot other_slot : ExpansionInterface::MEMCARD_SLOTS) + { + if (other_slot == slot) + continue; + + const std::string other_eu_path = Config::GetGCIFolderPath(other_slot, DiscIO::Region::PAL); + if (eu_path == other_eu_path) + { + ModalMessageBox::critical( + this, tr("Error"), + tr("The same folder can't be used in multiple slots; it is already used by %1.") + .arg(QString::fromStdString(fmt::to_string(other_slot)))); + return false; + } + } + + path_changed = eu_path != Config::GetGCIFolderPath(slot, DiscIO::Region::PAL); + } + + Config::SetBase(Config::GetInfoForGCIPath(slot), raw_path); + + if (Core::IsRunning()) + { + // If emulation is running and the new card is different from the old one, notify the system to + // eject the old and insert the new card. + // TODO: This should probably be done by a config change callback instead. + if (path_changed) + { + // 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::MemoryCardFolder); + } + } + + LoadSettings(); + return true; +} + void GameCubePane::BrowseAGPRom(ExpansionInterface::Slot slot) { ASSERT(ExpansionInterface::IsMemcardSlot(slot)); @@ -576,6 +706,8 @@ void GameCubePane::LoadSettings() ->setText(QString::fromStdString(Config::GetMemcardPath(slot, std::nullopt))); SignalBlocking(m_agp_paths[slot]) ->setText(QString::fromStdString(Config::Get(Config::GetInfoForAGPCartPath(slot)))); + SignalBlocking(m_gci_paths[slot]) + ->setText(QString::fromStdString(Config::GetGCIFolderPath(slot, std::nullopt))); } #ifdef HAS_LIBMGBA diff --git a/Source/Core/DolphinQt/Settings/GameCubePane.h b/Source/Core/DolphinQt/Settings/GameCubePane.h index 4ed1934e1c..fd3dd556fa 100644 --- a/Source/Core/DolphinQt/Settings/GameCubePane.h +++ b/Source/Core/DolphinQt/Settings/GameCubePane.h @@ -42,6 +42,8 @@ private: void BrowseMemcard(ExpansionInterface::Slot slot); bool SetMemcard(ExpansionInterface::Slot slot, const QString& filename); + void BrowseGCIFolder(ExpansionInterface::Slot slot); + bool SetGCIFolder(ExpansionInterface::Slot slot, const QString& path); void BrowseAGPRom(ExpansionInterface::Slot slot); void SetAGPRom(ExpansionInterface::Slot slot, const QString& filename); void BrowseGBABios(); @@ -63,6 +65,10 @@ private: Common::EnumMap m_agp_path_labels; Common::EnumMap m_agp_paths; + Common::EnumMap m_gci_path_layouts; + Common::EnumMap m_gci_path_labels; + Common::EnumMap m_gci_paths; + QCheckBox* m_gba_threads; QCheckBox* m_gba_save_rom_path; QPushButton* m_gba_browse_bios;