diff --git a/Source/Android/jni/GameList/GameFile.cpp b/Source/Android/jni/GameList/GameFile.cpp index b9f7a28df4..b43f7b4402 100644 --- a/Source/Android/jni/GameList/GameFile.cpp +++ b/Source/Android/jni/GameList/GameFile.cpp @@ -84,13 +84,13 @@ JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getPlatform JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getTitle(JNIEnv* env, jobject obj) { - return ToJString(env, GetRef(env, obj)->GetName()); + return ToJString(env, GetRef(env, obj)->GetName(true)); } JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getDescription(JNIEnv* env, jobject obj) { - return ToJString(env, GetRef(env, obj)->GetDescription()); + return ToJString(env, GetRef(env, obj)->GetDescription(true)); } JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getCompany(JNIEnv* env, diff --git a/Source/Core/DolphinQt/Config/InfoWidget.cpp b/Source/Core/DolphinQt/Config/InfoWidget.cpp index e7915636bb..ad96cc25f9 100644 --- a/Source/Core/DolphinQt/Config/InfoWidget.cpp +++ b/Source/Core/DolphinQt/Config/InfoWidget.cpp @@ -73,7 +73,7 @@ QGroupBox* InfoWidget::CreateISODetails() QLineEdit* country = CreateValueDisplay(DiscIO::GetName(m_game.GetCountry(), true)); - const std::string game_maker = m_game.GetMaker(); + const std::string game_maker = m_game.GetMaker(false); QLineEdit* maker = CreateValueDisplay((game_maker.empty() ? UNKNOWN_NAME.toStdString() : game_maker) + " (" + diff --git a/Source/Core/DolphinQt/GameList/GameList.cpp b/Source/Core/DolphinQt/GameList/GameList.cpp index e1a953a5dd..f75422f992 100644 --- a/Source/Core/DolphinQt/GameList/GameList.cpp +++ b/Source/Core/DolphinQt/GameList/GameList.cpp @@ -442,7 +442,7 @@ void GameList::ExportWiiSave() for (const auto& game : GetSelectedGames()) { if (!WiiSave::Export(game->GetTitleID(), export_dir.toStdString())) - failed.push_back(game->GetName()); + failed.push_back(game->GetName(true)); } if (!failed.isEmpty()) diff --git a/Source/Core/DolphinQt/GameList/GameListModel.cpp b/Source/Core/DolphinQt/GameList/GameListModel.cpp index ba060ad705..923befda47 100644 --- a/Source/Core/DolphinQt/GameList/GameListModel.cpp +++ b/Source/Core/DolphinQt/GameList/GameListModel.cpp @@ -124,13 +124,13 @@ QVariant GameListModel::data(const QModelIndex& index, int role) const case COL_DESCRIPTION: if (role == Qt::DisplayRole || role == Qt::InitialSortOrderRole) { - return QString::fromStdString(game.GetDescription()) + return QString::fromStdString(game.GetDescription(true)) .replace(QLatin1Char('\n'), QLatin1Char(' ')); } break; case COL_MAKER: if (role == Qt::DisplayRole || role == Qt::InitialSortOrderRole) - return QString::fromStdString(game.GetMaker()); + return QString::fromStdString(game.GetMaker(true)); break; case COL_FILE_NAME: if (role == Qt::DisplayRole || role == Qt::InitialSortOrderRole) diff --git a/Source/Core/UICommon/CMakeLists.txt b/Source/Core/UICommon/CMakeLists.txt index fe5f8f1599..cea2560394 100644 --- a/Source/Core/UICommon/CMakeLists.txt +++ b/Source/Core/UICommon/CMakeLists.txt @@ -32,6 +32,7 @@ PUBLIC common cpp-optparse minizip + pugixml PRIVATE $<$:${IOK_LIBRARY}> diff --git a/Source/Core/UICommon/GameFile.cpp b/Source/Core/UICommon/GameFile.cpp index f5aefd7f01..d29f5ad2ae 100644 --- a/Source/Core/UICommon/GameFile.cpp +++ b/Source/Core/UICommon/GameFile.cpp @@ -17,6 +17,8 @@ #include #include +#include + #include "Common/ChunkFile.h" #include "Common/CommonPaths.h" #include "Common/CommonTypes.h" @@ -317,6 +319,9 @@ void GameFile::DoState(PointerWrap& p) p.Do(m_disc_number); p.Do(m_apploader_date); + p.Do(m_custom_name); + p.Do(m_custom_description); + p.Do(m_custom_maker); m_volume_banner.DoState(p); m_custom_banner.DoState(p); m_default_cover.DoState(p); @@ -333,6 +338,58 @@ bool GameFile::IsElfOrDol() const return name_end == ".elf" || name_end == ".dol"; } +bool GameFile::ReadXMLMetadata(const std::string& path) +{ + std::string data; + if (!File::ReadFileToString(path, data)) + return false; + + pugi::xml_document doc; + // We use load_buffer instead of load_file to avoid path encoding problems on Windows + if (!doc.load_buffer(data.data(), data.size())) + return false; + + const pugi::xml_node app_node = doc.child("app"); + m_pending.custom_name = app_node.child("name").text().as_string(); + m_pending.custom_maker = app_node.child("coder").text().as_string(); + m_pending.custom_description = app_node.child("short_description").text().as_string(); + + // Elements that we aren't using: + // version (can be written in any format) + // release_date (YYYYmmddHHMMSS format) + // long_description (can be several screens long!) + + return true; +} + +bool GameFile::XMLMetadataChanged() +{ + std::string path, name; + SplitPath(m_file_path, &path, &name, nullptr); + + // This XML file naming format is intended as an alternative to the Homebrew Channel naming + // for those who don't want to have a Homebrew Channel style folder structure. + if (!ReadXMLMetadata(path + name + ".xml")) + { + // Homebrew Channel naming. Typical for DOLs and ELFs, but we also support it for volumes. + if (!ReadXMLMetadata(path + "meta.xml")) + { + // If no XML metadata is found, remove any old XML metadata from memory. + m_pending.custom_banner = {}; + } + } + + return m_pending.custom_name != m_custom_name && m_pending.custom_maker != m_custom_maker && + m_pending.custom_description != m_custom_description; +} + +void GameFile::XMLMetadataCommit() +{ + m_custom_name = std::move(m_pending.custom_name); + m_custom_description = std::move(m_pending.custom_description); + m_custom_maker = std::move(m_pending.custom_maker); +} + bool GameFile::WiiBannerChanged() { // Wii banners can only be read if there is a save file. @@ -389,7 +446,7 @@ bool GameFile::CustomBannerChanged() std::string path, name; SplitPath(m_file_path, &path, &name, nullptr); - // This icon naming format is intended as an alternative to Homebrew Channel icons + // This icon naming format is intended as an alternative to the Homebrew Channel naming // for those who don't want to have a Homebrew Channel style folder structure. if (!ReadPNGBanner(path + name + ".png")) { @@ -411,12 +468,18 @@ void GameFile::CustomBannerCommit() const std::string& GameFile::GetName(const Core::TitleDatabase& title_database) const { - const std::string& custom_name = title_database.GetTitleName(m_gametdb_id, GetConfigLanguage()); - return custom_name.empty() ? GetName() : custom_name; + if (!m_custom_name.empty()) + return m_custom_name; + + const std::string& database_name = title_database.GetTitleName(m_gametdb_id, GetConfigLanguage()); + return database_name.empty() ? GetName(true) : database_name; } -const std::string& GameFile::GetName(bool long_name) const +const std::string& GameFile::GetName(bool allow_custom_name, bool long_name) const { + if (allow_custom_name && !m_custom_name.empty()) + return m_custom_name; + const std::string& name = long_name ? GetLongName() : GetShortName(); if (!name.empty()) return name; @@ -425,8 +488,11 @@ const std::string& GameFile::GetName(bool long_name) const return m_file_name; } -const std::string& GameFile::GetMaker(bool long_maker) const +const std::string& GameFile::GetMaker(bool allow_custom_maker, bool long_maker) const { + if (allow_custom_maker && !m_custom_maker.empty()) + return m_custom_maker; + const std::string& maker = long_maker ? GetLongMaker() : GetShortMaker(); if (!maker.empty()) return maker; @@ -437,6 +503,14 @@ const std::string& GameFile::GetMaker(bool long_maker) const return EMPTY_STRING; } +const std::string& GameFile::GetDescription(bool allow_custom_description) const +{ + if (allow_custom_description && !m_custom_description.empty()) + return m_custom_description; + + return LookupUsingConfigLanguage(m_descriptions); +} + std::vector GameFile::GetLanguages() const { std::vector languages; diff --git a/Source/Core/UICommon/GameFile.h b/Source/Core/UICommon/GameFile.h index aced331685..f783d9cac0 100644 --- a/Source/Core/UICommon/GameFile.h +++ b/Source/Core/UICommon/GameFile.h @@ -52,8 +52,8 @@ public: const std::string& GetFilePath() const { return m_file_path; } const std::string& GetFileName() const { return m_file_name; } const std::string& GetName(const Core::TitleDatabase& title_database) const; - const std::string& GetName(bool long_name = true) const; - const std::string& GetMaker(bool long_maker = true) const; + const std::string& GetName(bool allow_custom_name, bool long_name = true) const; + const std::string& GetMaker(bool allow_custom_maker, bool long_maker = true) const; const std::string& GetShortName(DiscIO::Language l) const { return Lookup(l, m_short_names); } const std::string& GetShortName() const { return LookupUsingConfigLanguage(m_short_names); } const std::string& GetLongName(DiscIO::Language l) const { return Lookup(l, m_long_names); } @@ -63,7 +63,7 @@ public: const std::string& GetLongMaker(DiscIO::Language l) const { return Lookup(l, m_long_makers); } const std::string& GetLongMaker() const { return LookupUsingConfigLanguage(m_long_makers); } const std::string& GetDescription(DiscIO::Language l) const { return Lookup(l, m_descriptions); } - const std::string& GetDescription() const { return LookupUsingConfigLanguage(m_descriptions); } + const std::string& GetDescription(bool allow_custom_description) const; std::vector GetLanguages() const; const std::string& GetInternalName() const { return m_internal_name; } const std::string& GetGameID() const { return m_game_id; } @@ -85,6 +85,8 @@ public: const GameBanner& GetBannerImage() const; const GameCover& GetCoverImage() const; void DoState(PointerWrap& p); + bool XMLMetadataChanged(); + void XMLMetadataCommit(); bool WiiBannerChanged(); void WiiBannerCommit(); bool CustomBannerChanged(); @@ -102,6 +104,7 @@ private: const std::string& LookupUsingConfigLanguage(const std::map& strings) const; bool IsElfOrDol() const; + bool ReadXMLMetadata(const std::string& path); bool ReadPNGBanner(const std::string& path); // IMPORTANT: Nearly all data members must be save/restored in DoState. @@ -134,6 +137,9 @@ private: u8 m_disc_number{}; std::string m_apploader_date; + std::string m_custom_name; + std::string m_custom_description; + std::string m_custom_maker; GameBanner m_volume_banner{}; GameBanner m_custom_banner{}; GameCover m_default_cover{}; @@ -143,6 +149,9 @@ private: // of GameFiles in a threadsafe way. They should not be handled in DoState. struct { + std::string custom_name; + std::string custom_description; + std::string custom_maker; GameBanner volume_banner; GameBanner custom_banner; GameCover default_cover; diff --git a/Source/Core/UICommon/GameFileCache.cpp b/Source/Core/UICommon/GameFileCache.cpp index fa83ed66e0..07cbf726c4 100644 --- a/Source/Core/UICommon/GameFileCache.cpp +++ b/Source/Core/UICommon/GameFileCache.cpp @@ -27,7 +27,7 @@ namespace UICommon { -static constexpr u32 CACHE_REVISION = 15; // Last changed in PR 7816 +static constexpr u32 CACHE_REVISION = 16; // Last changed in PR 8313 std::vector FindAllGamePaths(const std::vector& directories_to_scan, bool recursive_scan) @@ -166,6 +166,7 @@ bool GameFileCache::UpdateAdditionalMetadata( bool GameFileCache::UpdateAdditionalMetadata(std::shared_ptr* game_file) { + const bool xml_metadata_changed = (*game_file)->XMLMetadataChanged(); const bool wii_banner_changed = (*game_file)->WiiBannerChanged(); const bool custom_banner_changed = (*game_file)->CustomBannerChanged(); @@ -174,14 +175,18 @@ bool GameFileCache::UpdateAdditionalMetadata(std::shared_ptr* game_fil const bool default_cover_changed = (*game_file)->DefaultCoverChanged(); const bool custom_cover_changed = (*game_file)->CustomCoverChanged(); - if (!wii_banner_changed && !custom_banner_changed && !default_cover_changed && - !custom_cover_changed) + if (!xml_metadata_changed && !wii_banner_changed && !custom_banner_changed && + !default_cover_changed && !custom_cover_changed) + { return false; + } // If a cached file needs an update, apply the updates to a copy and delete the original. // This makes the usage of cached files in other threads safe. std::shared_ptr copy = std::make_shared(**game_file); + if (xml_metadata_changed) + copy->XMLMetadataCommit(); if (wii_banner_changed) copy->WiiBannerCommit(); if (custom_banner_changed) diff --git a/Source/Core/UICommon/UICommon.vcxproj b/Source/Core/UICommon/UICommon.vcxproj index 7526d92b0b..5a76b39501 100644 --- a/Source/Core/UICommon/UICommon.vcxproj +++ b/Source/Core/UICommon/UICommon.vcxproj @@ -48,6 +48,9 @@ {4482FD2A-EC43-3FFB-AC20-2E5C54B05EAD} + + {38fee76f-f347-484b-949c-b4649381cffb} +