diff --git a/src/core/cdrom.h b/src/core/cdrom.h index 45c90cb39..840d76b06 100644 --- a/src/core/cdrom.h +++ b/src/core/cdrom.h @@ -30,7 +30,7 @@ public: bool DoState(StateWrapper& sw); bool HasMedia() const { return m_reader.HasMedia(); } - std::string GetMediaFileName() const { return m_reader.GetMediaFileName(); } + const std::string& GetMediaFileName() const { return m_reader.GetMediaFileName(); } void InsertMedia(std::unique_ptr media); std::unique_ptr RemoveMedia(bool force = false); diff --git a/src/core/cdrom_async_reader.h b/src/core/cdrom_async_reader.h index ef2804c17..2e13a4771 100644 --- a/src/core/cdrom_async_reader.h +++ b/src/core/cdrom_async_reader.h @@ -19,7 +19,7 @@ public: const CDImage::SubChannelQ& GetSectorSubQ() const { return m_subq; } const bool HasMedia() const { return static_cast(m_media); } const CDImage* GetMedia() const { return m_media.get(); } - const std::string GetMediaFileName() const { return m_media ? m_media->GetFileName() : std::string(); } + const std::string& GetMediaFileName() const { return m_media->GetFileName(); } bool IsUsingThread() const { return m_read_thread.joinable(); } void StartThread(); diff --git a/src/core/game_list.cpp b/src/core/game_list.cpp index 9e13cd2ca..3ff1f6fb6 100644 --- a/src/core/game_list.cpp +++ b/src/core/game_list.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -214,6 +215,58 @@ bool GameList::IsPsfFileName(const char* path) return (extension && StringUtil::Strcasecmp(extension, ".psf") == 0); } +bool GameList::IsM3UFileName(const char* path) +{ + const char* extension = std::strrchr(path, '.'); + return (extension && StringUtil::Strcasecmp(extension, ".m3u") == 0); +} + +std::vector GameList::ParseM3UFile(const char* path) +{ + std::ifstream ifs(path); + if (!ifs.is_open()) + { + Log_ErrorPrintf("Failed to open %s", path); + return {}; + } + + std::vector entries; + std::string line; + while (std::getline(ifs, line)) + { + u32 start_offset = 0; + while (start_offset < line.size() && std::isspace(line[start_offset])) + start_offset++; + + // skip comments + if (start_offset == line.size() || line[start_offset] == '#') + continue; + + // strip ending whitespace + u32 end_offset = static_cast(line.size()) - 1; + while (std::isspace(line[end_offset]) && end_offset > start_offset) + end_offset--; + + // anything? + if (start_offset == end_offset) + continue; + + std::string entry_path(line.begin() + start_offset, line.begin() + end_offset + 1); + if (!FileSystem::IsAbsolutePath(entry_path)) + { + SmallString absolute_path; + FileSystem::BuildPathRelativeToFile(absolute_path, path, entry_path.c_str()); + entry_path = absolute_path; + } + + Log_DevPrintf("Read path from m3u: '%s'", entry_path.c_str()); + entries.push_back(std::move(entry_path)); + } + + Log_InfoPrintf("Loaded %zu paths from m3u '%s'", entries.size(), path); + return entries; +} + const char* GameList::GetGameListCompatibilityRatingString(GameListCompatibilityRating rating) { static constexpr std::array(GameListCompatibilityRating::Count)> names = { diff --git a/src/core/game_list.h b/src/core/game_list.h index 1c47634ed..23bd9be53 100644 --- a/src/core/game_list.h +++ b/src/core/game_list.h @@ -77,6 +77,12 @@ public: /// Returns true if the filename is a Portable Sound Format file we can uncompress/load. static bool IsPsfFileName(const char* path); + /// Returns true if the filename is a M3U Playlist we can handle. + static bool IsM3UFileName(const char* path); + + /// Parses an M3U playlist, returning the entries. + static std::vector ParseM3UFile(const char* path); + /// Returns a string representation of a compatibility level. static const char* GetGameListCompatibilityRatingString(GameListCompatibilityRating rating); diff --git a/src/core/system.cpp b/src/core/system.cpp index 4ed19e5d3..1199f7bdf 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -26,6 +26,7 @@ #include "timers.h" #include #include +#include Log_SetChannel(System); #ifdef WIN32 @@ -175,8 +176,35 @@ bool System::Boot(const SystemBootParameters& params) } else { - Log_InfoPrintf("Loading CD image '%s'...", params.filename.c_str()); - media = OpenCDImage(params.filename.c_str(), params.override_load_image_to_ram.value_or(false)); + u32 playlist_index; + if (GameList::IsM3UFileName(params.filename.c_str())) + { + m_media_playlist = GameList::ParseM3UFile(params.filename.c_str()); + if (m_media_playlist.empty()) + { + m_host_interface->ReportFormattedError("Failed to parse playlist '%s'", params.filename.c_str()); + return false; + } + + if (params.media_playlist_index >= m_media_playlist.size()) + { + Log_WarningPrintf("Media playlist index %u out of range, using first", params.media_playlist_index); + playlist_index = 0; + } + else + { + playlist_index = params.media_playlist_index; + } + } + else + { + AddMediaPathToPlaylist(params.filename); + playlist_index = 0; + } + + const std::string& media_path = m_media_playlist[playlist_index]; + Log_InfoPrintf("Loading CD image '%s' from playlist index %u...", media_path.c_str(), playlist_index); + media = OpenCDImage(media_path.c_str(), params.load_image_to_ram); if (!media) { m_host_interface->ReportFormattedError("Failed to load CD image '%s'", params.filename.c_str()); @@ -518,11 +546,14 @@ bool System::SaveState(ByteStream* state, u32 screenshot_size /* = 128 */) StringUtil::Strlcpy(header.title, m_running_game_title.c_str(), sizeof(header.title)); StringUtil::Strlcpy(header.game_code, m_running_game_code.c_str(), sizeof(header.game_code)); - std::string media_filename = m_cdrom->GetMediaFileName(); - header.offset_to_media_filename = static_cast(state->GetPosition()); - header.media_filename_length = static_cast(media_filename.length()); - if (!media_filename.empty() && !state->Write2(media_filename.data(), header.media_filename_length)) - return false; + if (m_cdrom->HasMedia()) + { + const std::string& media_filename = m_cdrom->GetMediaFileName(); + header.offset_to_media_filename = static_cast(state->GetPosition()); + header.media_filename_length = static_cast(media_filename.length()); + if (!media_filename.empty() && !state->Write2(media_filename.data(), header.media_filename_length)) + return false; + } // save screenshot if (screenshot_size > 0) @@ -971,6 +1002,8 @@ bool System::InsertMedia(const char* path) UpdateRunningGame(path, image.get()); m_cdrom->InsertMedia(std::move(image)); + Log_InfoPrintf("Inserted media from %s (%s, %s)", m_running_game_path.c_str(), m_running_game_code.c_str(), + m_running_game_title.c_str()); if (GetSettings().HasAnyPerGameMemoryCards()) { @@ -1196,3 +1229,89 @@ void System::UpdateRunningGame(const char* path, CDImage* image) m_host_interface->OnRunningGameChanged(); } + +u32 System::GetMediaPlaylistIndex() const +{ + if (!m_cdrom->HasMedia()) + return std::numeric_limits::max(); + + const std::string& media_path = m_cdrom->GetMediaFileName(); + for (u32 i = 0; i < static_cast(m_media_playlist.size()); i++) + { + if (m_media_playlist[i] == media_path) + return i; + } + + return std::numeric_limits::max(); +} + +bool System::AddMediaPathToPlaylist(const std::string_view& path) +{ + if (std::any_of(m_media_playlist.begin(), m_media_playlist.end(), + [&path](const std::string& p) { return (path == p); })) + { + return false; + } + + m_media_playlist.emplace_back(path); + return true; +} + +bool System::RemoveMediaPathFromPlaylist(const std::string_view& path) +{ + for (u32 i = 0; i < static_cast(m_media_playlist.size()); i++) + { + if (path == m_media_playlist[i]) + return RemoveMediaPathFromPlaylist(i); + } + + return false; +} + +bool System::RemoveMediaPathFromPlaylist(u32 index) +{ + if (index >= static_cast(m_media_playlist.size())) + return false; + + if (GetMediaPlaylistIndex() == index) + { + m_host_interface->ReportMessage("Removing current media from playlist, removing media from CD-ROM."); + m_cdrom->RemoveMedia(); + } + + m_media_playlist.erase(m_media_playlist.begin() + index); + return true; +} + +bool System::ReplaceMediaPathFromPlaylist(u32 index, const std::string_view& path) +{ + if (index >= static_cast(m_media_playlist.size())) + return false; + + if (GetMediaPlaylistIndex() == index) + { + m_host_interface->ReportMessage("Changing current media from playlist, replacing current media."); + m_cdrom->RemoveMedia(); + + m_media_playlist[index] = path; + InsertMedia(m_media_playlist[index].c_str()); + } + else + { + m_media_playlist[index] = path; + } + + return true; +} + +bool System::SwitchMediaFromPlaylist(u32 index) +{ + if (index >= m_media_playlist.size()) + return false; + + const std::string& path = m_media_playlist[index]; + if (m_cdrom->HasMedia() && m_cdrom->GetMediaFileName() == path) + return true; + + return InsertMedia(path.c_str()); +} diff --git a/src/core/system.h b/src/core/system.h index 895c8c0b1..29378afe6 100644 --- a/src/core/system.h +++ b/src/core/system.h @@ -38,8 +38,9 @@ struct SystemBootParameters std::string filename; std::optional override_fast_boot; std::optional override_fullscreen; - std::optional override_load_image_to_ram; std::unique_ptr state_stream; + u32 media_playlist_index = 0; + bool load_image_to_ram = false; bool force_software_renderer = false; }; @@ -152,6 +153,28 @@ public: std::unique_ptr CreateTimingEvent(std::string name, TickCount period, TickCount interval, TimingEventCallback callback, bool activate); + /// Returns the number of entries in the media/disc playlist. + ALWAYS_INLINE u32 GetMediaPlaylistCount() const { return static_cast(m_media_playlist.size()); } + + /// Returns the current image from the media/disc playlist. + u32 GetMediaPlaylistIndex() const; + + /// Returns the path to the specified playlist index. + const std::string& GetMediaPlaylistPath(u32 index) const { return m_media_playlist[index]; } + + /// Adds a new path to the media playlist. + bool AddMediaPathToPlaylist(const std::string_view& path); + + /// Removes a path from the media playlist. + bool RemoveMediaPathFromPlaylist(const std::string_view& path); + bool RemoveMediaPathFromPlaylist(u32 index); + + /// Changes a path from the media playlist. + bool ReplaceMediaPathFromPlaylist(u32 index, const std::string_view& path); + + /// Switches to the specified media/disc playlist index. + bool SwitchMediaFromPlaylist(u32 index); + private: System(HostInterface* host_interface); @@ -241,4 +264,7 @@ private: u32 m_last_global_tick_counter = 0; Common::Timer m_fps_timer; Common::Timer m_frame_timer; + + // Playlist of disc images. + std::vector m_media_playlist; };