From bd665aad5d6924783a66495eadb0628157062748 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Mon, 5 Nov 2018 19:20:45 +0100 Subject: [PATCH 1/5] Automatic disc change for 2-disc games --- Source/Core/Core/Boot/Boot.cpp | 67 +++++++--- Source/Core/Core/Boot/Boot.h | 5 +- Source/Core/Core/Config/MainSettings.cpp | 1 + Source/Core/Core/Config/MainSettings.h | 1 + .../Core/ConfigLoaders/IsSettingSaveable.cpp | 1 + Source/Core/Core/HW/DVD/DVDInterface.cpp | 68 +++++++++- Source/Core/Core/HW/DVD/DVDInterface.h | 9 +- Source/Core/Core/Movie.cpp | 26 +--- Source/Core/DolphinNoGUI/MainNoGUI.cpp | 5 +- Source/Core/DolphinQt/GameList/GameList.cpp | 11 ++ Source/Core/DolphinQt/GameList/GameList.h | 2 + .../Core/DolphinQt/GameList/GameListModel.cpp | 35 ++++- .../Core/DolphinQt/GameList/GameListModel.h | 5 +- Source/Core/DolphinQt/Main.cpp | 5 +- Source/Core/DolphinQt/MainWindow.cpp | 124 ++++++++++++------ Source/Core/DolphinQt/MainWindow.h | 24 +++- .../Core/DolphinQt/Settings/GeneralPane.cpp | 6 + Source/Core/DolphinQt/Settings/GeneralPane.h | 1 + Source/Core/UICommon/CommandLineParse.cpp | 2 +- 19 files changed, 306 insertions(+), 92 deletions(-) diff --git a/Source/Core/Core/Boot/Boot.cpp b/Source/Core/Core/Boot/Boot.cpp index 618b644b38..c429b7ee2c 100644 --- a/Source/Core/Core/Boot/Boot.cpp +++ b/Source/Core/Core/Boot/Boot.cpp @@ -54,6 +54,12 @@ #include "DiscIO/Enums.h" #include "DiscIO/Volume.h" +std::vector ReadM3UFile(const std::string& path) +{ + // TODO + return {}; +} + BootParameters::BootParameters(Parameters&& parameters_, const std::optional& savestate_path_) : parameters(std::move(parameters_)), savestate_path(savestate_path_) @@ -61,40 +67,68 @@ BootParameters::BootParameters(Parameters&& parameters_, } std::unique_ptr -BootParameters::GenerateFromFile(const std::string& path, +BootParameters::GenerateFromFile(std::string boot_path, const std::optional& savestate_path) { - const bool is_drive = Common::IsCDROMDevice(path); + return GenerateFromFile(std::vector{std::move(boot_path)}, savestate_path); +} + +std::unique_ptr +BootParameters::GenerateFromFile(std::vector paths, + const std::optional& savestate_path) +{ + ASSERT(!paths.empty()); + + const bool is_drive = Common::IsCDROMDevice(paths.front()); // Check if the file exist, we may have gotten it from a --elf command line // that gave an incorrect file name - if (!is_drive && !File::Exists(path)) + if (!is_drive && !File::Exists(paths.front())) { - PanicAlertT("The specified file \"%s\" does not exist", path.c_str()); + PanicAlertT("The specified file \"%s\" does not exist", paths.front().c_str()); return {}; } std::string extension; - SplitPath(path, nullptr, nullptr, &extension); + SplitPath(paths.front(), nullptr, nullptr, &extension); std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower); + if (extension == ".m3u") + { + std::vector new_paths = ReadM3UFile(paths.front()); + if (!new_paths.empty()) + { + paths = new_paths; + + SplitPath(paths.front(), nullptr, nullptr, &extension); + std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower); + } + } + + const std::string path = paths.front(); + if (paths.size() == 1) + paths.clear(); + static const std::unordered_set disc_image_extensions = { {".gcm", ".iso", ".tgc", ".wbfs", ".ciso", ".gcz", ".dol", ".elf"}}; if (disc_image_extensions.find(extension) != disc_image_extensions.end() || is_drive) { std::unique_ptr volume = DiscIO::CreateVolumeFromFilename(path); if (volume) - return std::make_unique(Disc{path, std::move(volume)}, savestate_path); + { + return std::make_unique(Disc{std::move(path), std::move(volume), paths}, + savestate_path); + } if (extension == ".elf") { - return std::make_unique(Executable{path, std::make_unique(path)}, - savestate_path); + return std::make_unique( + Executable{std::move(path), std::make_unique(path)}, savestate_path); } if (extension == ".dol") { - return std::make_unique(Executable{path, std::make_unique(path)}, - savestate_path); + return std::make_unique( + Executable{std::move(path), std::make_unique(path)}, savestate_path); } if (is_drive) @@ -113,10 +147,10 @@ BootParameters::GenerateFromFile(const std::string& path, } if (extension == ".dff") - return std::make_unique(DFF{path}, savestate_path); + return std::make_unique(DFF{std::move(path)}, savestate_path); if (extension == ".wad") - return std::make_unique(DiscIO::WiiWAD{path}, savestate_path); + return std::make_unique(DiscIO::WiiWAD{std::move(path)}, savestate_path); PanicAlertT("Could not recognize file %s", path.c_str()); return {}; @@ -136,10 +170,11 @@ BootParameters::IPL::IPL(DiscIO::Region region_, Disc&& disc_) : IPL(region_) // Inserts a disc into the emulated disc drive and returns a pointer to it. // The returned pointer must only be used while we are still booting, // because DVDThread can do whatever it wants to the disc after that. -static const DiscIO::Volume* SetDisc(std::unique_ptr volume) +static const DiscIO::Volume* SetDisc(std::unique_ptr volume, + std::vector auto_disc_change_paths = {}) { const DiscIO::Volume* pointer = volume.get(); - DVDInterface::SetDisc(std::move(volume)); + DVDInterface::SetDisc(std::move(volume), auto_disc_change_paths); return pointer; } @@ -326,7 +361,7 @@ bool CBoot::BootUp(std::unique_ptr boot) bool operator()(BootParameters::Disc& disc) const { NOTICE_LOG(BOOT, "Booting from disc: %s", disc.path.c_str()); - const DiscIO::Volume* volume = SetDisc(std::move(disc.volume)); + const DiscIO::Volume* volume = SetDisc(std::move(disc.volume), disc.auto_disc_change_paths); if (!volume) return false; @@ -420,7 +455,7 @@ bool CBoot::BootUp(std::unique_ptr boot) if (ipl.disc) { NOTICE_LOG(BOOT, "Inserting disc: %s", ipl.disc->path.c_str()); - SetDisc(DiscIO::CreateVolumeFromFilename(ipl.disc->path)); + SetDisc(DiscIO::CreateVolumeFromFilename(ipl.disc->path), ipl.disc->auto_disc_change_paths); } if (LoadMapFromFilename()) diff --git a/Source/Core/Core/Boot/Boot.h b/Source/Core/Core/Boot/Boot.h index 589b9b2056..7cb7b39989 100644 --- a/Source/Core/Core/Boot/Boot.h +++ b/Source/Core/Core/Boot/Boot.h @@ -40,6 +40,7 @@ struct BootParameters { std::string path; std::unique_ptr volume; + std::vector auto_disc_change_paths; }; struct Executable @@ -69,7 +70,9 @@ struct BootParameters }; static std::unique_ptr - GenerateFromFile(const std::string& boot_path, + GenerateFromFile(std::string boot_path, const std::optional& savestate_path = {}); + static std::unique_ptr + GenerateFromFile(std::vector paths, const std::optional& savestate_path = {}); using Parameters = std::variant; diff --git a/Source/Core/Core/Config/MainSettings.cpp b/Source/Core/Core/Config/MainSettings.cpp index 155c8d4933..6dc1c45eb3 100644 --- a/Source/Core/Core/Config/MainSettings.cpp +++ b/Source/Core/Core/Config/MainSettings.cpp @@ -104,6 +104,7 @@ const ConfigInfo MAIN_CUSTOM_RTC_VALUE{{System::Main, "Core", "CustomRTCVal const ConfigInfo MAIN_ENABLE_SIGNATURE_CHECKS{{System::Main, "Core", "EnableSignatureChecks"}, true}; const ConfigInfo MAIN_REDUCE_POLLING_RATE{{System::Main, "Core", "ReducePollingRate"}, false}; +const ConfigInfo MAIN_AUTO_DISC_CHANGE{{System::Main, "Core", "AutoDiscChange"}, false}; // Main.DSP diff --git a/Source/Core/Core/Config/MainSettings.h b/Source/Core/Core/Config/MainSettings.h index 65f4662bc7..d932416eb7 100644 --- a/Source/Core/Core/Config/MainSettings.h +++ b/Source/Core/Core/Config/MainSettings.h @@ -78,6 +78,7 @@ extern const ConfigInfo MAIN_CUSTOM_RTC_ENABLE; extern const ConfigInfo MAIN_CUSTOM_RTC_VALUE; extern const ConfigInfo MAIN_ENABLE_SIGNATURE_CHECKS; extern const ConfigInfo MAIN_REDUCE_POLLING_RATE; +extern const ConfigInfo MAIN_AUTO_DISC_CHANGE; // Main.DSP diff --git a/Source/Core/Core/ConfigLoaders/IsSettingSaveable.cpp b/Source/Core/Core/ConfigLoaders/IsSettingSaveable.cpp index be51266c2f..146fdd1120 100644 --- a/Source/Core/Core/ConfigLoaders/IsSettingSaveable.cpp +++ b/Source/Core/Core/ConfigLoaders/IsSettingSaveable.cpp @@ -31,6 +31,7 @@ bool IsSettingSaveable(const Config::ConfigLocation& config_location) Config::MAIN_DEFAULT_ISO.location, Config::MAIN_MEMCARD_A_PATH.location, Config::MAIN_MEMCARD_B_PATH.location, + Config::MAIN_AUTO_DISC_CHANGE.location, // Graphics.Hardware diff --git a/Source/Core/Core/HW/DVD/DVDInterface.cpp b/Source/Core/Core/HW/DVD/DVDInterface.cpp index 2dda449bad..b335be618c 100644 --- a/Source/Core/Core/HW/DVD/DVDInterface.cpp +++ b/Source/Core/Core/HW/DVD/DVDInterface.cpp @@ -16,8 +16,10 @@ #include "Common/Align.h" #include "Common/ChunkFile.h" #include "Common/CommonTypes.h" +#include "Common/Config/Config.h" #include "Common/Logging/Log.h" +#include "Core/Config/MainSettings.h" #include "Core/ConfigManager.h" #include "Core/CoreTiming.h" #include "Core/HW/AudioInterface.h" @@ -36,6 +38,8 @@ #include "DiscIO/Volume.h" #include "DiscIO/VolumeWii.h" +#include "VideoCommon/OnScreenDisplay.h" + // The minimum time it takes for the DVD drive to process a command (in // microseconds) constexpr u64 COMMAND_LATENCY_US = 300; @@ -231,6 +235,8 @@ static u64 s_read_buffer_end_offset; // Disc changing static std::string s_disc_path_to_insert; +static std::vector s_auto_disc_change_paths; +static size_t s_auto_disc_change_index; // Events static CoreTiming::EventType* s_finish_executing_command; @@ -441,11 +447,21 @@ void Shutdown() DVDThread::Stop(); } -void SetDisc(std::unique_ptr disc) +void SetDisc(std::unique_ptr disc, + std::optional> auto_disc_change_paths = {}) { if (disc) s_current_partition = disc->GetGamePartition(); + if (auto_disc_change_paths) + { + ASSERT_MSG(DISCIO, (*auto_disc_change_paths).size() != 1, + "Cannot automatically change between one disc"); + + s_auto_disc_change_paths = *auto_disc_change_paths; + s_auto_disc_change_index = 0; + } + DVDThread::SetDisc(std::move(disc)); SetLidOpen(); } @@ -457,7 +473,7 @@ bool IsDiscInside() static void EjectDiscCallback(u64 userdata, s64 cyclesLate) { - SetDisc(nullptr); + SetDisc(nullptr, {}); } static void InsertDiscCallback(u64 userdata, s64 cyclesLate) @@ -466,7 +482,7 @@ static void InsertDiscCallback(u64 userdata, s64 cyclesLate) DiscIO::CreateVolumeFromFilename(s_disc_path_to_insert); if (new_volume) - SetDisc(std::move(new_volume)); + SetDisc(std::move(new_volume), {}); else PanicAlertT("The disc that was about to be inserted couldn't be found."); @@ -479,6 +495,20 @@ void EjectDisc() CoreTiming::ScheduleEvent(0, s_eject_disc); } +// Must only be called on the CPU thread +void ChangeDisc(const std::vector& paths) +{ + ASSERT_MSG(DISCIO, !paths.empty(), "Trying to insert an empty list of discs"); + + if (paths.size() > 1) + { + s_auto_disc_change_paths = paths; + s_auto_disc_change_index = 0; + } + + ChangeDisc(paths[0]); +} + // Must only be called on the CPU thread void ChangeDisc(const std::string& new_path) { @@ -493,6 +523,28 @@ void ChangeDisc(const std::string& new_path) s_disc_path_to_insert = new_path; CoreTiming::ScheduleEvent(SystemTimers::GetTicksPerSecond(), s_insert_disc); Movie::SignalDiscChange(new_path); + + for (size_t i = 0; i < s_auto_disc_change_paths.size(); ++i) + { + if (s_auto_disc_change_paths[i] == new_path) + { + s_auto_disc_change_index = i; + return; + } + } + + s_auto_disc_change_paths.clear(); +} + +// Must only be called on the CPU thread +bool AutoChangeDisc() +{ + if (s_auto_disc_change_paths.empty()) + return false; + + s_auto_disc_change_index = (s_auto_disc_change_index + 1) % s_auto_disc_change_paths.size(); + ChangeDisc(s_auto_disc_change_paths[s_auto_disc_change_index]); + return true; } void SetLidOpen() @@ -983,12 +1035,20 @@ void ExecuteCommand(u32 command_0, u32 command_1, u32 command_2, u32 output_addr break; case DVDLowStopMotor: + { INFO_LOG(DVDINTERFACE, "DVDLowStopMotor %s %s", command_1 ? "eject" : "", command_2 ? "kill!" : ""); - if (command_1 && !command_2) + bool auto_disc_change = Config::Get(Config::MAIN_AUTO_DISC_CHANGE) && !Movie::IsPlayingInput(); + if (auto_disc_change) + auto_disc_change = AutoChangeDisc(); + if (auto_disc_change) + OSD::AddMessage("Changing discs automatically...", OSD::Duration::NORMAL); + + if (!auto_disc_change && command_1 && !command_2) EjectDiscCallback(0, 0); break; + } // DVD Audio Enable/Disable (Immediate). GC uses this, and apparently Wii also does...? case DVDLowAudioBufferConfig: diff --git a/Source/Core/Core/HW/DVD/DVDInterface.h b/Source/Core/Core/HW/DVD/DVDInterface.h index fc23fb8bbd..e345dd07a6 100644 --- a/Source/Core/Core/HW/DVD/DVDInterface.h +++ b/Source/Core/Core/HW/DVD/DVDInterface.h @@ -111,10 +111,13 @@ void DoState(PointerWrap& p); void RegisterMMIO(MMIO::Mapping* mmio, u32 base); -void SetDisc(std::unique_ptr disc); +void SetDisc(std::unique_ptr disc, + std::optional> auto_disc_change_paths); bool IsDiscInside(); -void EjectDisc(); // Must only be called on the CPU thread -void ChangeDisc(const std::string& new_path); // Must only be called on the CPU thread +void EjectDisc(); // Must only be called on the CPU thread +void ChangeDisc(const std::vector& paths); // Must only be called on the CPU thread +void ChangeDisc(const std::string& new_path); // Must only be called on the CPU thread +bool AutoChangeDisc(); // Must only be called on the CPU thread // This function returns true and calls SConfig::SetRunningGameMetadata(Volume&, Partition&) // if both of the following conditions are true: diff --git a/Source/Core/Core/Movie.cpp b/Source/Core/Core/Movie.cpp index d779f0a71d..ae12021e47 100644 --- a/Source/Core/Core/Movie.cpp +++ b/Source/Core/Core/Movie.cpp @@ -1176,29 +1176,13 @@ void PlayController(GCPadStatus* PadStatus, int controllerID) PadStatus->button |= PAD_TRIGGER_R; if (s_padState.disc) { - // This implementation assumes the disc change will only happen once. Trying - // to change more than that will cause it to load the last disc every time. - // As far as I know, there are no 3+ disc games, so this should be fine. - bool found = false; - std::string path; - for (const std::string& iso_folder : SConfig::GetInstance().m_ISOFolder) - { - path = iso_folder + '/' + s_discChange; - if (File::Exists(path)) + Core::RunAsCPUThread([] { + if (!DVDInterface::AutoChangeDisc()) { - found = true; - break; + CPU::Break(); + PanicAlertT("Change the disc to %s", s_discChange.c_str()); } - } - if (found) - { - Core::RunAsCPUThread([&path] { DVDInterface::ChangeDisc(path); }); - } - else - { - CPU::Break(); - PanicAlertT("Change the disc to %s", s_discChange.c_str()); - } + }); } if (s_padState.reset) diff --git a/Source/Core/DolphinNoGUI/MainNoGUI.cpp b/Source/Core/DolphinNoGUI/MainNoGUI.cpp index 6cd71bd353..697e4cbe6d 100644 --- a/Source/Core/DolphinNoGUI/MainNoGUI.cpp +++ b/Source/Core/DolphinNoGUI/MainNoGUI.cpp @@ -368,7 +368,10 @@ int main(int argc, char* argv[]) std::unique_ptr boot; if (options.is_set("exec")) { - boot = BootParameters::GenerateFromFile(static_cast(options.get("exec"))); + const std::list paths_list = options.all("exec"); + const std::vector paths{std::make_move_iterator(std::begin(paths_list)), + std::make_move_iterator(std::end(paths_list))}; + boot = BootParameters::GenerateFromFile(paths); } else if (options.is_set("nand_title")) { diff --git a/Source/Core/DolphinQt/GameList/GameList.cpp b/Source/Core/DolphinQt/GameList/GameList.cpp index c2a098281f..0af57f92ba 100644 --- a/Source/Core/DolphinQt/GameList/GameList.cpp +++ b/Source/Core/DolphinQt/GameList/GameList.cpp @@ -737,6 +737,17 @@ bool GameList::HasMultipleSelected() const m_grid->selectionModel()->selectedIndexes().size() > 1; } +std::shared_ptr GameList::FindGame(const std::string& path) const +{ + return m_model->FindGame(path); +} + +std::shared_ptr +GameList::FindSecondDisc(const UICommon::GameFile& game) const +{ + return m_model->FindSecondDisc(game); +} + void GameList::SetViewColumn(int col, bool view) { m_list->setColumnHidden(col, !view); diff --git a/Source/Core/DolphinQt/GameList/GameList.h b/Source/Core/DolphinQt/GameList/GameList.h index fac9932c3c..e7782651be 100644 --- a/Source/Core/DolphinQt/GameList/GameList.h +++ b/Source/Core/DolphinQt/GameList/GameList.h @@ -30,6 +30,8 @@ public: std::shared_ptr GetSelectedGame() const; QList> GetSelectedGames() const; bool HasMultipleSelected() const; + std::shared_ptr FindGame(const std::string& path) const; + std::shared_ptr FindSecondDisc(const UICommon::GameFile& game) const; void SetListView() { SetPreferredView(true); } void SetGridView() { SetPreferredView(false); } diff --git a/Source/Core/DolphinQt/GameList/GameListModel.cpp b/Source/Core/DolphinQt/GameList/GameListModel.cpp index 43bc61f9a1..e7440ce1e2 100644 --- a/Source/Core/DolphinQt/GameList/GameListModel.cpp +++ b/Source/Core/DolphinQt/GameList/GameListModel.cpp @@ -278,7 +278,7 @@ void GameListModel::AddGame(const std::shared_ptr& gam void GameListModel::UpdateGame(const std::shared_ptr& game) { - int index = FindGame(game->GetFilePath()); + int index = FindGameIndex(game->GetFilePath()); if (index < 0) { AddGame(game); @@ -292,7 +292,7 @@ void GameListModel::UpdateGame(const std::shared_ptr& void GameListModel::RemoveGame(const std::string& path) { - int entry = FindGame(path); + int entry = FindGameIndex(path); if (entry < 0) return; @@ -301,7 +301,13 @@ void GameListModel::RemoveGame(const std::string& path) endRemoveRows(); } -int GameListModel::FindGame(const std::string& path) const +std::shared_ptr GameListModel::FindGame(const std::string& path) const +{ + const int index = FindGameIndex(path); + return index < 0 ? nullptr : m_games[index]; +} + +int GameListModel::FindGameIndex(const std::string& path) const { for (int i = 0; i < m_games.size(); i++) { @@ -311,6 +317,29 @@ int GameListModel::FindGame(const std::string& path) const return -1; } +std::shared_ptr +GameListModel::FindSecondDisc(const UICommon::GameFile& game) const +{ + std::shared_ptr match_without_revision = nullptr; + + if (DiscIO::IsDisc(game.GetPlatform())) + { + for (auto& other_game : m_games) + { + if (game.GetGameID() == other_game->GetGameID() && + game.GetDiscNumber() != other_game->GetDiscNumber()) + { + if (game.GetRevision() == other_game->GetRevision()) + return other_game; + else + match_without_revision = other_game; + } + } + } + + return match_without_revision; +} + void GameListModel::SetSearchTerm(const QString& term) { m_term = term; diff --git a/Source/Core/DolphinQt/GameList/GameListModel.h b/Source/Core/DolphinQt/GameList/GameListModel.h index 7defb3f9cb..de0501d53b 100644 --- a/Source/Core/DolphinQt/GameList/GameListModel.h +++ b/Source/Core/DolphinQt/GameList/GameListModel.h @@ -63,6 +63,9 @@ public: void UpdateGame(const std::shared_ptr& game); void RemoveGame(const std::string& path); + std::shared_ptr FindGame(const std::string& path) const; + std::shared_ptr FindSecondDisc(const UICommon::GameFile& game) const; + void SetScale(float scale); float GetScale() const; @@ -79,7 +82,7 @@ public: private: // Index in m_games, or -1 if it isn't found - int FindGame(const std::string& path) const; + int FindGameIndex(const std::string& path) const; QStringList m_tag_list; QMap m_game_tags; diff --git a/Source/Core/DolphinQt/Main.cpp b/Source/Core/DolphinQt/Main.cpp index 4c7aa086ff..5ca745e874 100644 --- a/Source/Core/DolphinQt/Main.cpp +++ b/Source/Core/DolphinQt/Main.cpp @@ -147,7 +147,10 @@ int main(int argc, char* argv[]) std::unique_ptr boot; if (options.is_set("exec")) { - boot = BootParameters::GenerateFromFile(static_cast(options.get("exec"))); + const std::list paths_list = options.all("exec"); + const std::vector paths{std::make_move_iterator(std::begin(paths_list)), + std::make_move_iterator(std::end(paths_list))}; + boot = BootParameters::GenerateFromFile(paths); } else if (options.is_set("nand_title")) { diff --git a/Source/Core/DolphinQt/MainWindow.cpp b/Source/Core/DolphinQt/MainWindow.cpp index 1b57cbe96c..ed4379ea98 100644 --- a/Source/Core/DolphinQt/MainWindow.cpp +++ b/Source/Core/DolphinQt/MainWindow.cpp @@ -168,6 +168,17 @@ static WindowSystemInfo GetWindowSystemInfo(QWindow* window) return wsi; } +static std::vector StringListToStdVector(QStringList list) +{ + std::vector result; + result.reserve(list.size()); + + for (const QString& s : list) + result.push_back(s.toStdString()); + + return result; +} + MainWindow::MainWindow(std::unique_ptr boot_parameters) : QMainWindow(nullptr) { setWindowTitle(QString::fromStdString(Common::scm_rev_str)); @@ -387,7 +398,7 @@ void MainWindow::ConnectMenuBar() connect(m_menu_bar, &MenuBar::EjectDisc, this, &MainWindow::EjectDisc); connect(m_menu_bar, &MenuBar::ChangeDisc, this, &MainWindow::ChangeDisc); connect(m_menu_bar, &MenuBar::BootDVDBackup, this, - [this](const QString& drive) { StartGame(drive); }); + [this](const QString& drive) { StartGame(drive, ScanForSecondDisc::No); }); // Emulation connect(m_menu_bar, &MenuBar::Pause, this, &MainWindow::Pause); @@ -610,30 +621,30 @@ void MainWindow::RefreshGameList() Settings::Instance().RefreshGameList(); } -QString MainWindow::PromptFileName() +QStringList MainWindow::PromptFileNames() { auto& settings = Settings::Instance().GetQSettings(); - QString path = QFileDialog::getOpenFileName( + QStringList paths = QFileDialog::getOpenFileNames( this, tr("Select a File"), settings.value(QStringLiteral("mainwindow/lastdir"), QStringLiteral("")).toString(), tr("All GC/Wii files (*.elf *.dol *.gcm *.iso *.tgc *.wbfs *.ciso *.gcz *.wad *.dff);;" "All Files (*)")); - if (!path.isEmpty()) + if (!paths.isEmpty()) { settings.setValue(QStringLiteral("mainwindow/lastdir"), - QFileInfo(path).absoluteDir().absolutePath()); + QFileInfo(paths.front()).absoluteDir().absolutePath()); } - return path; + return paths; } void MainWindow::ChangeDisc() { - QString file = PromptFileName(); + std::vector paths = StringListToStdVector(PromptFileNames()); - if (!file.isEmpty()) - Core::RunAsCPUThread([&file] { DVDInterface::ChangeDisc(file.toStdString()); }); + if (!paths.empty()) + Core::RunAsCPUThread([&paths] { DVDInterface::ChangeDisc(paths); }); } void MainWindow::EjectDisc() @@ -643,9 +654,9 @@ void MainWindow::EjectDisc() void MainWindow::Open() { - QString file = PromptFileName(); - if (!file.isEmpty()) - StartGame(file); + QStringList files = PromptFileNames(); + if (!files.isEmpty()) + StartGame(StringListToStdVector(files)); } void MainWindow::Play(const std::optional& savestate_path) @@ -664,7 +675,7 @@ void MainWindow::Play(const std::optional& savestate_path) std::shared_ptr selection = m_game_list->GetSelectedGame(); if (selection) { - StartGame(selection->GetFilePath(), savestate_path); + StartGame(selection->GetFilePath(), ScanForSecondDisc::Yes, savestate_path); EnableScreenSaver(false); } else @@ -672,7 +683,7 @@ void MainWindow::Play(const std::optional& savestate_path) const QString default_path = QString::fromStdString(Config::Get(Config::MAIN_DEFAULT_ISO)); if (!default_path.isEmpty() && QFile::exists(default_path)) { - StartGame(default_path, savestate_path); + StartGame(default_path, ScanForSecondDisc::Yes, savestate_path); EnableScreenSaver(false); } else @@ -833,17 +844,46 @@ void MainWindow::ScreenShot() Core::SaveScreenShot(); } -void MainWindow::StartGame(const QString& path, const std::optional& savestate_path) +void MainWindow::ScanForSecondDiscAndStartGame(const UICommon::GameFile& game, + const std::optional& savestate_path) { - StartGame(path.toStdString(), savestate_path); + auto second_game = m_game_list->FindSecondDisc(game); + + std::vector paths = {game.GetFilePath()}; + if (second_game != nullptr) + paths.push_back(second_game->GetFilePath()); + + StartGame(paths, savestate_path); } -void MainWindow::StartGame(const std::string& path, +void MainWindow::StartGame(const QString& path, ScanForSecondDisc scan, const std::optional& savestate_path) { + StartGame(path.toStdString(), scan, savestate_path); +} + +void MainWindow::StartGame(const std::string& path, ScanForSecondDisc scan, + const std::optional& savestate_path) +{ + if (scan == ScanForSecondDisc::Yes) + { + std::shared_ptr game = m_game_list->FindGame(path); + if (game != nullptr) + { + ScanForSecondDiscAndStartGame(*game, savestate_path); + return; + } + } + StartGame(BootParameters::GenerateFromFile(path, savestate_path)); } +void MainWindow::StartGame(const std::vector& paths, + const std::optional& savestate_path) +{ + StartGame(BootParameters::GenerateFromFile(paths, savestate_path)); +} + void MainWindow::StartGame(std::unique_ptr&& parameters) { // If we're running, only start a new game once we've stopped the last. @@ -1075,7 +1115,7 @@ void MainWindow::ShowFIFOPlayer() { m_fifo_window = new FIFOPlayerWindow(this); connect(m_fifo_window, &FIFOPlayerWindow::LoadFIFORequested, this, - [this](const QString& path) { StartGame(path); }); + [this](const QString& path) { StartGame(path, ScanForSecondDisc::No); }); } m_fifo_window->show(); @@ -1170,7 +1210,7 @@ void MainWindow::NetPlayInit() #endif connect(m_netplay_dialog, &NetPlayDialog::Boot, this, - [this](const QString& path) { StartGame(path); }); + [this](const QString& path) { StartGame(path, ScanForSecondDisc::Yes); }); connect(m_netplay_dialog, &NetPlayDialog::Stop, this, &MainWindow::ForceStop); connect(m_netplay_dialog, &NetPlayDialog::rejected, this, &MainWindow::NetPlayQuit); connect(m_netplay_setup_dialog, &NetPlaySetupDialog::Join, this, &MainWindow::NetPlayJoin); @@ -1346,38 +1386,48 @@ void MainWindow::dragEnterEvent(QDragEnterEvent* event) void MainWindow::dropEvent(QDropEvent* event) { - const auto& urls = event->mimeData()->urls(); + const QList& urls = event->mimeData()->urls(); if (urls.empty()) return; - const auto& url = urls[0]; - QFileInfo file_info(url.toLocalFile()); + QStringList files; + QStringList folders; - auto path = file_info.filePath(); - - if (!file_info.exists() || !file_info.isReadable()) + for (const QUrl& url : urls) { - QMessageBox::critical(this, tr("Error"), tr("Failed to open '%1'").arg(path)); - return; + QFileInfo file_info(url.toLocalFile()); + QString path = file_info.filePath(); + + if (!file_info.exists() || !file_info.isReadable()) + { + QMessageBox::critical(this, tr("Error"), tr("Failed to open '%1'").arg(path)); + return; + } + + (file_info.isFile() ? files : folders).append(path); } - if (file_info.isFile()) + if (!files.isEmpty()) { - StartGame(path); + StartGame(StringListToStdVector(files)); } else { - auto& settings = Settings::Instance(); + Settings& settings = Settings::Instance(); + const bool show_confirm = settings.GetPaths().size() != 0; - if (settings.GetPaths().size() != 0) + for (const QString& folder : folders) { - if (QMessageBox::question( - this, tr("Confirm"), - tr("Do you want to add \"%1\" to the list of Game Paths?").arg(path)) != - QMessageBox::Yes) - return; + if (show_confirm) + { + if (QMessageBox::question( + this, tr("Confirm"), + tr("Do you want to add \"%1\" to the list of Game Paths?").arg(folder)) != + QMessageBox::Yes) + return; + } + settings.AddPath(folder); } - settings.AddPath(path); } } diff --git a/Source/Core/DolphinQt/MainWindow.h b/Source/Core/DolphinQt/MainWindow.h index 30c29030db..4c5224fb6a 100644 --- a/Source/Core/DolphinQt/MainWindow.h +++ b/Source/Core/DolphinQt/MainWindow.h @@ -5,6 +5,7 @@ #pragma once #include +#include #include #include @@ -47,6 +48,11 @@ namespace DiscIO enum class Region; } +namespace UICommon +{ +class GameFile; +} + namespace X11Utils { class XRRConfiguration; @@ -115,8 +121,20 @@ private: void InitCoreCallbacks(); - void StartGame(const QString& path, const std::optional& savestate_path = {}); - void StartGame(const std::string& path, const std::optional& savestate_path = {}); + enum class ScanForSecondDisc + { + Yes, + No, + }; + + void ScanForSecondDiscAndStartGame(const UICommon::GameFile& game, + const std::optional& savestate_path = {}); + void StartGame(const QString& path, ScanForSecondDisc scan, + const std::optional& savestate_path = {}); + void StartGame(const std::string& path, ScanForSecondDisc scan, + const std::optional& savestate_path = {}); + void StartGame(const std::vector& paths, + const std::optional& savestate_path = {}); void StartGame(std::unique_ptr&& parameters); void ShowRenderWidget(); void HideRenderWidget(bool reinit = true); @@ -155,7 +173,7 @@ private: void ChangeDisc(); void EjectDisc(); - QString PromptFileName(); + QStringList PromptFileNames(); void EnableScreenSaver(bool enable); diff --git a/Source/Core/DolphinQt/Settings/GeneralPane.cpp b/Source/Core/DolphinQt/Settings/GeneralPane.cpp index d98d5cb04e..78ec034fb3 100644 --- a/Source/Core/DolphinQt/Settings/GeneralPane.cpp +++ b/Source/Core/DolphinQt/Settings/GeneralPane.cpp @@ -96,6 +96,7 @@ void GeneralPane::ConnectLayout() { connect(m_checkbox_dualcore, &QCheckBox::toggled, this, &GeneralPane::OnSaveConfig); connect(m_checkbox_cheats, &QCheckBox::toggled, this, &GeneralPane::OnSaveConfig); + connect(m_checkbox_auto_disc_change, &QCheckBox::toggled, this, &GeneralPane::OnSaveConfig); #ifdef USE_DISCORD_PRESENCE connect(m_checkbox_discord_presence, &QCheckBox::toggled, this, &GeneralPane::OnSaveConfig); #endif @@ -137,6 +138,9 @@ void GeneralPane::CreateBasic() m_checkbox_cheats = new QCheckBox(tr("Enable Cheats")); basic_group_layout->addWidget(m_checkbox_cheats); + m_checkbox_auto_disc_change = new QCheckBox(tr("Change Discs Automatically")); + basic_group_layout->addWidget(m_checkbox_auto_disc_change); + #ifdef USE_DISCORD_PRESENCE m_checkbox_discord_presence = new QCheckBox(tr("Show Current Game on Discord")); basic_group_layout->addWidget(m_checkbox_discord_presence); @@ -236,6 +240,7 @@ void GeneralPane::LoadConfig() #endif m_checkbox_dualcore->setChecked(SConfig::GetInstance().bCPUThread); m_checkbox_cheats->setChecked(Settings::Instance().GetCheatsEnabled()); + m_checkbox_auto_disc_change->setChecked(Config::Get(Config::MAIN_AUTO_DISC_CHANGE)); #ifdef USE_DISCORD_PRESENCE m_checkbox_discord_presence->setChecked(Config::Get(Config::MAIN_USE_DISCORD_PRESENCE)); #endif @@ -295,6 +300,7 @@ void GeneralPane::OnSaveConfig() settings.bCPUThread = m_checkbox_dualcore->isChecked(); Config::SetBaseOrCurrent(Config::MAIN_CPU_THREAD, m_checkbox_dualcore->isChecked()); Settings::Instance().SetCheatsEnabled(m_checkbox_cheats->isChecked()); + Config::SetBase(Config::MAIN_AUTO_DISC_CHANGE, m_checkbox_auto_disc_change->isChecked()); Config::SetBaseOrCurrent(Config::MAIN_ENABLE_CHEATS, m_checkbox_cheats->isChecked()); settings.m_EmulationSpeed = m_combobox_speedlimit->currentIndex() * 0.1f; diff --git a/Source/Core/DolphinQt/Settings/GeneralPane.h b/Source/Core/DolphinQt/Settings/GeneralPane.h index 90d53838e7..9ae0d48893 100644 --- a/Source/Core/DolphinQt/Settings/GeneralPane.h +++ b/Source/Core/DolphinQt/Settings/GeneralPane.h @@ -44,6 +44,7 @@ private: QComboBox* m_combobox_update_track; QCheckBox* m_checkbox_dualcore; QCheckBox* m_checkbox_cheats; + QCheckBox* m_checkbox_auto_disc_change; #ifdef USE_DISCORD_PRESENCE QCheckBox* m_checkbox_discord_presence; #endif diff --git a/Source/Core/UICommon/CommandLineParse.cpp b/Source/Core/UICommon/CommandLineParse.cpp index bc8da4d92b..69f1491ad7 100644 --- a/Source/Core/UICommon/CommandLineParse.cpp +++ b/Source/Core/UICommon/CommandLineParse.cpp @@ -75,7 +75,7 @@ std::unique_ptr CreateParser(ParserOptions options) parser->add_option("-u", "--user").action("store").help("User folder path"); parser->add_option("-m", "--movie").action("store").help("Play a movie file"); parser->add_option("-e", "--exec") - .action("store") + .action("append") .metavar("") .type("string") .help("Load the specified file"); From b608e80d8e5b3b99249b156369b9ceb1db4ad3d7 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Thu, 20 Dec 2018 17:52:41 +0100 Subject: [PATCH 2/5] Don't do automatic disc switching when running e.g. the Wii Menu We only want automatic disc switching to happen when the game actually is running, but software like the Wii Menu also uses DVDLowStopMotor. --- Source/Core/Core/HW/DVD/DVDInterface.cpp | 3 ++- Source/Core/Core/HW/DVD/DVDThread.cpp | 10 ++++++++++ Source/Core/Core/HW/DVD/DVDThread.h | 1 + 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Source/Core/Core/HW/DVD/DVDInterface.cpp b/Source/Core/Core/HW/DVD/DVDInterface.cpp index b335be618c..f83aa54bbc 100644 --- a/Source/Core/Core/HW/DVD/DVDInterface.cpp +++ b/Source/Core/Core/HW/DVD/DVDInterface.cpp @@ -1039,7 +1039,8 @@ void ExecuteCommand(u32 command_0, u32 command_1, u32 command_2, u32 output_addr INFO_LOG(DVDINTERFACE, "DVDLowStopMotor %s %s", command_1 ? "eject" : "", command_2 ? "kill!" : ""); - bool auto_disc_change = Config::Get(Config::MAIN_AUTO_DISC_CHANGE) && !Movie::IsPlayingInput(); + bool auto_disc_change = Config::Get(Config::MAIN_AUTO_DISC_CHANGE) && + !Movie::IsPlayingInput() && DVDThread::IsInsertedDiscRunning(); if (auto_disc_change) auto_disc_change = AutoChangeDisc(); if (auto_disc_change) diff --git a/Source/Core/Core/HW/DVD/DVDThread.cpp b/Source/Core/Core/HW/DVD/DVDThread.cpp index 3693bb9fe3..27f6df8be6 100644 --- a/Source/Core/Core/HW/DVD/DVDThread.cpp +++ b/Source/Core/Core/HW/DVD/DVDThread.cpp @@ -216,6 +216,16 @@ IOS::ES::TicketReader GetTicket(const DiscIO::Partition& partition) return s_disc->GetTicket(partition); } +bool IsInsertedDiscRunning() +{ + if (!s_disc) + return false; + + WaitUntilIdle(); + + return SConfig::GetInstance().GetGameID() == s_disc->GetGameID(); +} + bool UpdateRunningGameMetadata(const DiscIO::Partition& partition, std::optional title_id) { if (!s_disc) diff --git a/Source/Core/Core/HW/DVD/DVDThread.h b/Source/Core/Core/HW/DVD/DVDThread.h index 3a158cad1c..cdf066cc46 100644 --- a/Source/Core/Core/HW/DVD/DVDThread.h +++ b/Source/Core/Core/HW/DVD/DVDThread.h @@ -47,6 +47,7 @@ DiscIO::Platform GetDiscType(); u64 PartitionOffsetToRawOffset(u64 offset, const DiscIO::Partition& partition); IOS::ES::TMDReader GetTMD(const DiscIO::Partition& partition); IOS::ES::TicketReader GetTicket(const DiscIO::Partition& partition); +bool IsInsertedDiscRunning(); // This function returns true and calls SConfig::SetRunningGameMetadata(Volume&, Partition&) // if both of the following conditions are true: // - A disc is inserted From 352ac91a1cd2071d37ec47be4e5f937875bf9ad3 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Mon, 24 Dec 2018 14:32:35 +0100 Subject: [PATCH 3/5] Add a delay before automatically switching discs Some games don't behave as expected if we eject the disc as soon as we receive the DVDLowStopMotor command. For instance, Baten Kaitos never shows the prompt to switch discs or the "Reading disc..." text (but works correctly other than that). --- Source/Core/Core/HW/DVD/DVDInterface.cpp | 26 +++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/Source/Core/Core/HW/DVD/DVDInterface.cpp b/Source/Core/Core/HW/DVD/DVDInterface.cpp index f83aa54bbc..599620a9c7 100644 --- a/Source/Core/Core/HW/DVD/DVDInterface.cpp +++ b/Source/Core/Core/HW/DVD/DVDInterface.cpp @@ -240,9 +240,11 @@ static size_t s_auto_disc_change_index; // Events static CoreTiming::EventType* s_finish_executing_command; +static CoreTiming::EventType* s_auto_change_disc; static CoreTiming::EventType* s_eject_disc; static CoreTiming::EventType* s_insert_disc; +static void AutoChangeDiscCallback(u64 userdata, s64 cyclesLate); static void EjectDiscCallback(u64 userdata, s64 cyclesLate); static void InsertDiscCallback(u64 userdata, s64 cyclesLate); static void FinishExecutingCommandCallback(u64 userdata, s64 cycles_late); @@ -398,6 +400,7 @@ void Init() Reset(); s_DICVR.Hex = 1; // Disc Channel relies on cover being open when no disc is inserted + s_auto_change_disc = CoreTiming::RegisterEvent("AutoChangeDisc", AutoChangeDiscCallback); s_eject_disc = CoreTiming::RegisterEvent("EjectDisc", EjectDiscCallback); s_insert_disc = CoreTiming::RegisterEvent("InsertDisc", InsertDiscCallback); @@ -471,6 +474,11 @@ bool IsDiscInside() return DVDThread::HasDisc(); } +static void AutoChangeDiscCallback(u64 userdata, s64 cyclesLate) +{ + AutoChangeDisc(); +} + static void EjectDiscCallback(u64 userdata, s64 cyclesLate) { SetDisc(nullptr, {}); @@ -1039,15 +1047,19 @@ void ExecuteCommand(u32 command_0, u32 command_1, u32 command_2, u32 output_addr INFO_LOG(DVDINTERFACE, "DVDLowStopMotor %s %s", command_1 ? "eject" : "", command_2 ? "kill!" : ""); - bool auto_disc_change = Config::Get(Config::MAIN_AUTO_DISC_CHANGE) && - !Movie::IsPlayingInput() && DVDThread::IsInsertedDiscRunning(); - if (auto_disc_change) - auto_disc_change = AutoChangeDisc(); - if (auto_disc_change) - OSD::AddMessage("Changing discs automatically...", OSD::Duration::NORMAL); + const bool force_eject = command_1 && !command_2; - if (!auto_disc_change && command_1 && !command_2) + if (Config::Get(Config::MAIN_AUTO_DISC_CHANGE) && !Movie::IsPlayingInput() && + DVDThread::IsInsertedDiscRunning() && !s_auto_disc_change_paths.empty()) + { + CoreTiming::ScheduleEvent(force_eject ? 0 : SystemTimers::GetTicksPerSecond() / 2, + s_auto_change_disc); + OSD::AddMessage("Changing discs automatically...", OSD::Duration::NORMAL); + } + else if (force_eject) + { EjectDiscCallback(0, 0); + } break; } From 63c9831b939979c990580589e10e87784d1967e9 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Tue, 25 Dec 2018 13:33:22 +0100 Subject: [PATCH 4/5] Add Android support for automatic disc changing --- .../dolphinemu/dolphinemu/NativeLibrary.java | 4 +-- .../activities/EmulationActivity.java | 26 +++++++++++++------ .../ui/SettingsFragmentPresenter.java | 4 +++ .../features/settings/utils/SettingsFile.java | 1 + .../fragments/EmulationFragment.java | 21 +++++++-------- .../dolphinemu/dolphinemu/model/GameFile.java | 4 +++ .../services/GameFileCacheService.java | 20 ++++++++++++++ .../app/src/main/res/values/strings.xml | 1 + .../jni/AndroidCommon/AndroidCommon.cpp | 13 ++++++++++ .../Android/jni/AndroidCommon/AndroidCommon.h | 1 + Source/Android/jni/GameList/GameFile.cpp | 16 ++++++++++++ Source/Android/jni/MainAndroid.cpp | 19 +++++++------- 12 files changed, 100 insertions(+), 30 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.java index 425a749d75..6574d22199 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.java @@ -341,12 +341,12 @@ public final class NativeLibrary /** * Begins emulation. */ - public static native void Run(String path, boolean firstOpen); + public static native void Run(String[] path, boolean firstOpen); /** * Begins emulation from the specified savestate. */ - public static native void Run(String path, String savestatePath, boolean deleteSavestate); + public static native void Run(String[] path, String savestatePath, boolean deleteSavestate); public static native void ChangeDisc(String path); diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java index 9aa8955416..deadc4a9e9 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java @@ -37,6 +37,7 @@ import org.dolphinemu.dolphinemu.fragments.EmulationFragment; import org.dolphinemu.dolphinemu.fragments.MenuFragment; import org.dolphinemu.dolphinemu.fragments.SaveLoadStateFragment; import org.dolphinemu.dolphinemu.model.GameFile; +import org.dolphinemu.dolphinemu.services.GameFileCacheService; import org.dolphinemu.dolphinemu.ui.main.MainActivity; import org.dolphinemu.dolphinemu.ui.main.MainPresenter; import org.dolphinemu.dolphinemu.ui.platform.Platform; @@ -74,10 +75,10 @@ public final class EmulationActivity extends AppCompatActivity private boolean activityRecreated; private String mSelectedTitle; private int mPlatform; - private String mPath; + private String[] mPaths; private boolean backPressedOnce = false; - public static final String EXTRA_SELECTED_GAME = "SelectedGame"; + public static final String EXTRA_SELECTED_GAMES = "SelectedGames"; public static final String EXTRA_SELECTED_TITLE = "SelectedTitle"; public static final String EXTRA_PLATFORM = "Platform"; @@ -166,11 +167,20 @@ public final class EmulationActivity extends AppCompatActivity .append(R.id.menu_emulation_reset_overlay, EmulationActivity.MENU_ACTION_RESET_OVERLAY); } + private static String[] scanForSecondDisc(GameFile gameFile) + { + GameFile secondFile = GameFileCacheService.findSecondDisc(gameFile); + if (secondFile == null) + return new String[]{gameFile.getPath()}; + else + return new String[]{gameFile.getPath(), secondFile.getPath()}; + } + public static void launch(FragmentActivity activity, GameFile gameFile) { Intent launcher = new Intent(activity, EmulationActivity.class); - launcher.putExtra(EXTRA_SELECTED_GAME, gameFile.getPath()); + launcher.putExtra(EXTRA_SELECTED_GAMES, scanForSecondDisc(gameFile)); launcher.putExtra(EXTRA_SELECTED_TITLE, gameFile.getTitle()); launcher.putExtra(EXTRA_PLATFORM, gameFile.getPlatform()); Bundle options = new Bundle(); @@ -193,7 +203,7 @@ public final class EmulationActivity extends AppCompatActivity { // Get params we were passed Intent gameToEmulate = getIntent(); - mPath = gameToEmulate.getStringExtra(EXTRA_SELECTED_GAME); + mPaths = gameToEmulate.getStringArrayExtra(EXTRA_SELECTED_GAMES); mSelectedTitle = gameToEmulate.getStringExtra(EXTRA_SELECTED_TITLE); mPlatform = gameToEmulate.getIntExtra(EXTRA_PLATFORM, 0); activityRecreated = false; @@ -201,7 +211,7 @@ public final class EmulationActivity extends AppCompatActivity else { // Could have recreated the activity(rotate) before creating the fragment. If the fragment - // doesn't exist, treat this as a new start. + // doesn't exist, treat this as a new start. activityRecreated = mEmulationFragment != null; restoreState(savedInstanceState); } @@ -264,7 +274,7 @@ public final class EmulationActivity extends AppCompatActivity getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) && mEmulationFragment == null) { - mEmulationFragment = EmulationFragment.newInstance(mPath); + mEmulationFragment = EmulationFragment.newInstance(mPaths); getSupportFragmentManager().beginTransaction() .add(R.id.frame_emulation_fragment, mEmulationFragment) .commit(); @@ -286,7 +296,7 @@ public final class EmulationActivity extends AppCompatActivity { mEmulationFragment.saveTemporaryState(); } - outState.putString(EXTRA_SELECTED_GAME, mPath); + outState.putStringArray(EXTRA_SELECTED_GAMES, mPaths); outState.putString(EXTRA_SELECTED_TITLE, mSelectedTitle); outState.putInt(EXTRA_PLATFORM, mPlatform); super.onSaveInstanceState(outState); @@ -294,7 +304,7 @@ public final class EmulationActivity extends AppCompatActivity protected void restoreState(Bundle savedInstanceState) { - mPath = savedInstanceState.getString(EXTRA_SELECTED_GAME); + mPaths = savedInstanceState.getStringArray(EXTRA_SELECTED_GAMES); mSelectedTitle = savedInstanceState.getString(EXTRA_SELECTED_TITLE); mPlatform = savedInstanceState.getInt(EXTRA_PLATFORM); } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.java index 19ef69c5be..462c3bb00c 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.java @@ -213,6 +213,7 @@ public final class SettingsFragmentPresenter Setting overclock = null; Setting speedLimit = null; Setting audioStretch = null; + Setting autoDiscChange = null; Setting analytics = null; Setting enableSaveState; Setting lockToLandscape; @@ -225,6 +226,7 @@ public final class SettingsFragmentPresenter overclock = coreSection.getSetting(SettingsFile.KEY_OVERCLOCK_PERCENT); speedLimit = coreSection.getSetting(SettingsFile.KEY_SPEED_LIMIT); audioStretch = coreSection.getSetting(SettingsFile.KEY_AUDIO_STRETCH); + autoDiscChange = coreSection.getSetting(SettingsFile.KEY_AUTO_DISC_CHANGE); analytics = analyticsSection.getSetting(SettingsFile.KEY_ANALYTICS_ENABLED); enableSaveState = coreSection.getSetting(SettingsFile.KEY_ENABLE_SAVE_STATES); lockToLandscape = coreSection.getSetting(SettingsFile.KEY_LOCK_LANDSCAPE); @@ -264,6 +266,8 @@ public final class SettingsFragmentPresenter R.string.speed_limit, 0, 200, "%", 100, speedLimit)); sl.add(new CheckBoxSetting(SettingsFile.KEY_AUDIO_STRETCH, Settings.SECTION_INI_CORE, R.string.audio_stretch, R.string.audio_stretch_description, false, audioStretch)); + sl.add(new CheckBoxSetting(SettingsFile.KEY_AUTO_DISC_CHANGE, Settings.SECTION_INI_CORE, + R.string.auto_disc_change, 0, false, autoDiscChange)); sl.add(new CheckBoxSetting(SettingsFile.KEY_ENABLE_SAVE_STATES, Settings.SECTION_INI_CORE, R.string.enable_save_states, R.string.enable_save_states_description, false, enableSaveState)); diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/utils/SettingsFile.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/utils/SettingsFile.java index 322e2f0ae8..deb09cebe6 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/utils/SettingsFile.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/utils/SettingsFile.java @@ -45,6 +45,7 @@ public final class SettingsFile public static final String KEY_SPEED_LIMIT = "EmulationSpeed"; public static final String KEY_VIDEO_BACKEND = "GFXBackend"; public static final String KEY_AUDIO_STRETCH = "AudioStretch"; + public static final String KEY_AUTO_DISC_CHANGE = "AutoDiscChange"; public static final String KEY_GAME_CUBE_LANGUAGE = "SelectedLanguage"; public static final String KEY_OVERRIDE_GAME_CUBE_LANGUAGE = "OverrideGCLang"; public static final String KEY_SLOT_A_DEVICE = "SlotA"; diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.java index 38187f881d..dc888d74d4 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.java @@ -30,7 +30,7 @@ import java.io.File; public final class EmulationFragment extends Fragment implements SurfaceHolder.Callback { - private static final String KEY_GAMEPATH = "gamepath"; + private static final String KEY_GAMEPATHS = "gamepaths"; private SharedPreferences mPreferences; @@ -42,11 +42,10 @@ public final class EmulationFragment extends Fragment implements SurfaceHolder.C private EmulationActivity activity; - public static EmulationFragment newInstance(String gamePath) + public static EmulationFragment newInstance(String[] gamePaths) { - Bundle args = new Bundle(); - args.putString(KEY_GAMEPATH, gamePath); + args.putStringArray(KEY_GAMEPATHS, gamePaths); EmulationFragment fragment = new EmulationFragment(); fragment.setArguments(args); @@ -82,13 +81,13 @@ public final class EmulationFragment extends Fragment implements SurfaceHolder.C mPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); - String gamePath = getArguments().getString(KEY_GAMEPATH); + String[] gamePaths = getArguments().getStringArray(KEY_GAMEPATHS); SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); boolean firstOpen = preferences.getBoolean(StartupHandler.NEW_SESSION, true); SharedPreferences.Editor sPrefsEditor = preferences.edit(); sPrefsEditor.putBoolean(StartupHandler.NEW_SESSION, false); sPrefsEditor.apply(); - mEmulationState = new EmulationState(gamePath, getTemporaryStateFilePath(), firstOpen); + mEmulationState = new EmulationState(gamePaths, getTemporaryStateFilePath(), firstOpen); } /** @@ -273,7 +272,7 @@ public final class EmulationFragment extends Fragment implements SurfaceHolder.C STOPPED, RUNNING, PAUSED } - private final String mGamePath; + private final String[] mGamePaths; private Thread mEmulationThread; private State state; private Surface mSurface; @@ -282,10 +281,10 @@ public final class EmulationFragment extends Fragment implements SurfaceHolder.C private boolean firstOpen; private final String temporaryStatePath; - EmulationState(String gamePath, String temporaryStatePath, boolean firstOpen) + EmulationState(String[] gamePaths, String temporaryStatePath, boolean firstOpen) { this.firstOpen = firstOpen; - mGamePath = gamePath; + mGamePaths = gamePaths; this.temporaryStatePath = temporaryStatePath; // Starting state is stopped. state = State.STOPPED; @@ -423,12 +422,12 @@ public final class EmulationFragment extends Fragment implements SurfaceHolder.C if (loadPreviousTemporaryState) { Log.debug("[EmulationFragment] Starting emulation thread from previous state."); - NativeLibrary.Run(mGamePath, temporaryStatePath, true); + NativeLibrary.Run(mGamePaths, temporaryStatePath, true); } else { Log.debug("[EmulationFragment] Starting emulation thread."); - NativeLibrary.Run(mGamePath, firstOpen); + NativeLibrary.Run(mGamePaths, firstOpen); } }, "NativeEmulation"); mEmulationThread.start(); diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameFile.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameFile.java index 5565753632..45d0eb9060 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameFile.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameFile.java @@ -30,6 +30,10 @@ public class GameFile public native String getGameId(); + public native int getDiscNumber(); + + public native int getRevision(); + public native int[] getBanner(); public native int getBannerWidth(); diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/GameFileCacheService.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/GameFileCacheService.java index fa800a96c1..17aef0cd35 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/GameFileCacheService.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/GameFileCacheService.java @@ -61,6 +61,26 @@ public final class GameFileCacheService extends IntentService return null; } + public static GameFile findSecondDisc(GameFile game) + { + GameFile matchWithoutRevision = null; + + GameFile[] allGames = gameFiles.get(); + for (GameFile otherGame : allGames) + { + if (game.getGameId().equals(otherGame.getGameId()) && + game.getDiscNumber() != otherGame.getDiscNumber()) + { + if (game.getRevision() == otherGame.getRevision()) + return otherGame; + else + matchWithoutRevision = otherGame; + } + } + + return matchWithoutRevision; + } + private static void startService(Context context, String action) { Intent intent = new Intent(context, GameFileCacheService.class); diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index f387ad55f5..4949cc4137 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -136,6 +136,7 @@ Enable sound output through the speaker on a real Wiimote (DolphinBar required). Audio Stretching Stretches audio to reduce stuttering. Increases latency. + Change Discs Automatically Enable Savestates WARNING: Savestates may not be compatible with future versions of Dolphin and can make it impossible to create normal saves in some cases. Never use savestates as the only way of saving your progress. Lock screen to landscape diff --git a/Source/Android/jni/AndroidCommon/AndroidCommon.cpp b/Source/Android/jni/AndroidCommon/AndroidCommon.cpp index 350ea5483e..04e34c0f21 100644 --- a/Source/Android/jni/AndroidCommon/AndroidCommon.cpp +++ b/Source/Android/jni/AndroidCommon/AndroidCommon.cpp @@ -5,6 +5,7 @@ #include "jni/AndroidCommon/AndroidCommon.h" #include +#include #include @@ -24,3 +25,15 @@ jstring ToJString(JNIEnv* env, const std::string& str) { return env->NewStringUTF(str.c_str()); } + +std::vector JStringArrayToVector(JNIEnv* env, jobjectArray array) +{ + const jsize size = env->GetArrayLength(array); + std::vector result; + result.reserve(size); + + for (jsize i = 0; i < size; ++i) + result.push_back(GetJString(env, (jstring)env->GetObjectArrayElement(array, i))); + + return result; +} diff --git a/Source/Android/jni/AndroidCommon/AndroidCommon.h b/Source/Android/jni/AndroidCommon/AndroidCommon.h index 25ea59ed30..7b7d1bfc6b 100644 --- a/Source/Android/jni/AndroidCommon/AndroidCommon.h +++ b/Source/Android/jni/AndroidCommon/AndroidCommon.h @@ -10,3 +10,4 @@ std::string GetJString(JNIEnv* env, jstring jstr); jstring ToJString(JNIEnv* env, const std::string& str); +std::vector JStringArrayToVector(JNIEnv* env, jobjectArray array); diff --git a/Source/Android/jni/GameList/GameFile.cpp b/Source/Android/jni/GameList/GameFile.cpp index c11afa8c60..b84426a1ca 100644 --- a/Source/Android/jni/GameList/GameFile.cpp +++ b/Source/Android/jni/GameList/GameFile.cpp @@ -58,6 +58,10 @@ JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getPath( jobject obj); JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getGameId(JNIEnv* env, jobject obj); +JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getDiscNumber(JNIEnv* env, + jobject obj); +JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getRevision(JNIEnv* env, + jobject obj); JNIEXPORT jintArray JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getBanner(JNIEnv* env, jobject obj); JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getBannerWidth(JNIEnv* env, @@ -119,6 +123,18 @@ JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getGameI return ToJString(env, GetRef(env, obj)->GetGameID()); } +JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getDiscNumber(JNIEnv* env, + jobject obj) +{ + return env, GetRef(env, obj)->GetDiscNumber(); +} + +JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getRevision(JNIEnv* env, + jobject obj) +{ + return env, GetRef(env, obj)->GetRevision(); +} + JNIEXPORT jintArray JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getBanner(JNIEnv* env, jobject obj) { diff --git a/Source/Android/jni/MainAndroid.cpp b/Source/Android/jni/MainAndroid.cpp index f4692af117..c606345abe 100644 --- a/Source/Android/jni/MainAndroid.cpp +++ b/Source/Android/jni/MainAndroid.cpp @@ -571,10 +571,11 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_RefreshWiimo WiimoteReal::Refresh(); } -static void Run(const std::string& path, bool first_open, +static void Run(const std::vector& paths, bool first_open, std::optional savestate_path = {}, bool delete_savestate = false) { - __android_log_print(ANDROID_LOG_INFO, DOLPHIN_TAG, "Running : %s", path.c_str()); + ASSERT(!paths.empty()); + __android_log_print(ANDROID_LOG_INFO, DOLPHIN_TAG, "Running : %s", paths[0].c_str()); // Install our callbacks OSD::AddCallback(OSD::CallbackType::Shutdown, ButtonManager::Shutdown); @@ -595,7 +596,7 @@ static void Run(const std::string& path, bool first_open, // No use running the loop when booting fails s_have_wm_user_stop = false; - std::unique_ptr boot = BootParameters::GenerateFromFile(path, savestate_path); + std::unique_ptr boot = BootParameters::GenerateFromFile(paths, savestate_path); boot->delete_savestate = delete_savestate; WindowSystemInfo wsi(WindowSystemType::Android, nullptr, s_surf); if (BootManager::BootCore(std::move(boot), wsi)) @@ -630,17 +631,17 @@ static void Run(const std::string& path, bool first_open, } } -JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_Run__Ljava_lang_String_2Z( - JNIEnv* env, jobject obj, jstring jFile, jboolean jfirstOpen) +JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_Run___3Ljava_lang_String_2Z( + JNIEnv* env, jobject obj, jobjectArray jPaths, jboolean jfirstOpen) { - Run(GetJString(env, jFile), jfirstOpen); + Run(JStringArrayToVector(env, jPaths), jfirstOpen); } JNIEXPORT void JNICALL -Java_org_dolphinemu_dolphinemu_NativeLibrary_Run__Ljava_lang_String_2Ljava_lang_String_2Z( - JNIEnv* env, jobject obj, jstring jFile, jstring jSavestate, jboolean jDeleteSavestate) +Java_org_dolphinemu_dolphinemu_NativeLibrary_Run___3Ljava_lang_String_2Ljava_lang_String_2Z( + JNIEnv* env, jobject obj, jobjectArray jPaths, jstring jSavestate, jboolean jDeleteSavestate) { - Run(GetJString(env, jFile), false, GetJString(env, jSavestate), jDeleteSavestate); + Run(JStringArrayToVector(env, jPaths), false, GetJString(env, jSavestate), jDeleteSavestate); } JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_ChangeDisc(JNIEnv* env, From 0c622929ba0cf1d9c7030029ad86505024e5bbaa Mon Sep 17 00:00:00 2001 From: JosJuice Date: Tue, 25 Dec 2018 16:32:58 +0100 Subject: [PATCH 5/5] Add M3U file support for automatic disc switching --- Source/Core/Core/Boot/Boot.cpp | 71 +++++++++++++++++---- Source/Core/DolphinQt/Info.plist.in | 1 + Source/Core/DolphinQt/MainWindow.cpp | 2 +- Source/Core/DolphinQt/Settings/PathPane.cpp | 2 +- 4 files changed, 62 insertions(+), 14 deletions(-) diff --git a/Source/Core/Core/Boot/Boot.cpp b/Source/Core/Core/Boot/Boot.cpp index c429b7ee2c..6533a2719a 100644 --- a/Source/Core/Core/Boot/Boot.cpp +++ b/Source/Core/Core/Boot/Boot.cpp @@ -4,6 +4,12 @@ #include "Core/Boot/Boot.h" +#ifdef _MSC_VER +#include +namespace fs = std::experimental::filesystem; +#define HAS_STD_FILESYSTEM +#endif + #include #include #include @@ -54,10 +60,52 @@ #include "DiscIO/Enums.h" #include "DiscIO/Volume.h" -std::vector ReadM3UFile(const std::string& path) +std::vector ReadM3UFile(const std::string& m3u_path, const std::string& folder_path) { - // TODO - return {}; +#ifndef HAS_STD_FILESYSTEM + ASSERT(folder_path.back() == '/'); +#endif + + std::vector result; + std::vector nonexistent; + + std::ifstream s; + File::OpenFStream(s, m3u_path, std::ios_base::in); + + std::string line; + while (std::getline(s, line)) + { + if (StringBeginsWith(line, u8"\uFEFF")) + { + WARN_LOG(BOOT, "UTF-8 BOM in file: %s", m3u_path.c_str()); + line.erase(0, 3); + } + + if (!line.empty() && line.front() != '#') // Comments start with # + { +#ifdef HAS_STD_FILESYSTEM + const fs::path path_line = fs::u8path(line); + const std::string path_to_add = + path_line.is_relative() ? fs::u8path(folder_path).append(path_line).u8string() : line; +#else + const std::string path_to_add = line.front() != '/' ? folder_path + line : line; +#endif + + (File::Exists(path_to_add) ? result : nonexistent).push_back(path_to_add); + } + } + + if (!nonexistent.empty()) + { + PanicAlertT("Files specified in the M3U file \"%s\" were not found:\n%s", m3u_path.c_str(), + JoinStrings(nonexistent, "\n").c_str()); + return {}; + } + + if (result.empty()) + PanicAlertT("No paths found in the M3U file \"%s\"", m3u_path.c_str()); + + return result; } BootParameters::BootParameters(Parameters&& parameters_, @@ -88,20 +136,19 @@ BootParameters::GenerateFromFile(std::vector paths, return {}; } + std::string folder_path; std::string extension; - SplitPath(paths.front(), nullptr, nullptr, &extension); + SplitPath(paths.front(), &folder_path, nullptr, &extension); std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower); - if (extension == ".m3u") + if (extension == ".m3u" || extension == ".m3u8") { - std::vector new_paths = ReadM3UFile(paths.front()); - if (!new_paths.empty()) - { - paths = new_paths; + paths = ReadM3UFile(paths.front(), folder_path); + if (paths.empty()) + return {}; - SplitPath(paths.front(), nullptr, nullptr, &extension); - std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower); - } + SplitPath(paths.front(), nullptr, nullptr, &extension); + std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower); } const std::string path = paths.front(); diff --git a/Source/Core/DolphinQt/Info.plist.in b/Source/Core/DolphinQt/Info.plist.in index 08fcfc511f..7fd60fa8e0 100644 --- a/Source/Core/DolphinQt/Info.plist.in +++ b/Source/Core/DolphinQt/Info.plist.in @@ -13,6 +13,7 @@ gcm gcz iso + m3u tgc wad wbfs diff --git a/Source/Core/DolphinQt/MainWindow.cpp b/Source/Core/DolphinQt/MainWindow.cpp index ed4379ea98..bab9e280da 100644 --- a/Source/Core/DolphinQt/MainWindow.cpp +++ b/Source/Core/DolphinQt/MainWindow.cpp @@ -627,7 +627,7 @@ QStringList MainWindow::PromptFileNames() QStringList paths = QFileDialog::getOpenFileNames( this, tr("Select a File"), settings.value(QStringLiteral("mainwindow/lastdir"), QStringLiteral("")).toString(), - tr("All GC/Wii files (*.elf *.dol *.gcm *.iso *.tgc *.wbfs *.ciso *.gcz *.wad *.dff);;" + tr("All GC/Wii files (*.elf *.dol *.gcm *.iso *.tgc *.wbfs *.ciso *.gcz *.wad *.dff *.m3u);;" "All Files (*)")); if (!paths.isEmpty()) diff --git a/Source/Core/DolphinQt/Settings/PathPane.cpp b/Source/Core/DolphinQt/Settings/PathPane.cpp index 97e6f63b2b..9338751f6f 100644 --- a/Source/Core/DolphinQt/Settings/PathPane.cpp +++ b/Source/Core/DolphinQt/Settings/PathPane.cpp @@ -43,7 +43,7 @@ void PathPane::BrowseDefaultGame() { QString file = QDir::toNativeSeparators(QFileDialog::getOpenFileName( this, tr("Select a Game"), Settings::Instance().GetDefaultGame(), - tr("All GC/Wii files (*.elf *.dol *.gcm *.iso *.tgc *.wbfs *.ciso *.gcz *.wad);;" + tr("All GC/Wii files (*.elf *.dol *.gcm *.iso *.tgc *.wbfs *.ciso *.gcz *.wad *.m3u);;" "All Files (*)"))); if (!file.isEmpty())