GameFile: Support HBC-style XML metadata
This feature was originally exclusive to the previous iteration of DolphinQt (the one that was the reason for the current iteration being named DolphinQt2 initially). https://bugs.dolphin-emu.org/issues/8949
This commit is contained in:
parent
edfb0f66b6
commit
59f27ae4e1
|
@ -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,
|
JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getTitle(JNIEnv* env,
|
||||||
jobject obj)
|
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,
|
JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getDescription(JNIEnv* env,
|
||||||
jobject obj)
|
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,
|
JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getCompany(JNIEnv* env,
|
||||||
|
|
|
@ -73,7 +73,7 @@ QGroupBox* InfoWidget::CreateISODetails()
|
||||||
|
|
||||||
QLineEdit* country = CreateValueDisplay(DiscIO::GetName(m_game.GetCountry(), true));
|
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 =
|
QLineEdit* maker =
|
||||||
CreateValueDisplay((game_maker.empty() ? UNKNOWN_NAME.toStdString() : game_maker) + " (" +
|
CreateValueDisplay((game_maker.empty() ? UNKNOWN_NAME.toStdString() : game_maker) + " (" +
|
||||||
|
|
|
@ -442,7 +442,7 @@ void GameList::ExportWiiSave()
|
||||||
for (const auto& game : GetSelectedGames())
|
for (const auto& game : GetSelectedGames())
|
||||||
{
|
{
|
||||||
if (!WiiSave::Export(game->GetTitleID(), export_dir.toStdString()))
|
if (!WiiSave::Export(game->GetTitleID(), export_dir.toStdString()))
|
||||||
failed.push_back(game->GetName());
|
failed.push_back(game->GetName(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!failed.isEmpty())
|
if (!failed.isEmpty())
|
||||||
|
|
|
@ -124,13 +124,13 @@ QVariant GameListModel::data(const QModelIndex& index, int role) const
|
||||||
case COL_DESCRIPTION:
|
case COL_DESCRIPTION:
|
||||||
if (role == Qt::DisplayRole || role == Qt::InitialSortOrderRole)
|
if (role == Qt::DisplayRole || role == Qt::InitialSortOrderRole)
|
||||||
{
|
{
|
||||||
return QString::fromStdString(game.GetDescription())
|
return QString::fromStdString(game.GetDescription(true))
|
||||||
.replace(QLatin1Char('\n'), QLatin1Char(' '));
|
.replace(QLatin1Char('\n'), QLatin1Char(' '));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case COL_MAKER:
|
case COL_MAKER:
|
||||||
if (role == Qt::DisplayRole || role == Qt::InitialSortOrderRole)
|
if (role == Qt::DisplayRole || role == Qt::InitialSortOrderRole)
|
||||||
return QString::fromStdString(game.GetMaker());
|
return QString::fromStdString(game.GetMaker(true));
|
||||||
break;
|
break;
|
||||||
case COL_FILE_NAME:
|
case COL_FILE_NAME:
|
||||||
if (role == Qt::DisplayRole || role == Qt::InitialSortOrderRole)
|
if (role == Qt::DisplayRole || role == Qt::InitialSortOrderRole)
|
||||||
|
|
|
@ -32,6 +32,7 @@ PUBLIC
|
||||||
common
|
common
|
||||||
cpp-optparse
|
cpp-optparse
|
||||||
minizip
|
minizip
|
||||||
|
pugixml
|
||||||
|
|
||||||
PRIVATE
|
PRIVATE
|
||||||
$<$<BOOL:APPLE>:${IOK_LIBRARY}>
|
$<$<BOOL:APPLE>:${IOK_LIBRARY}>
|
||||||
|
|
|
@ -17,6 +17,8 @@
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include <pugixml.hpp>
|
||||||
|
|
||||||
#include "Common/ChunkFile.h"
|
#include "Common/ChunkFile.h"
|
||||||
#include "Common/CommonPaths.h"
|
#include "Common/CommonPaths.h"
|
||||||
#include "Common/CommonTypes.h"
|
#include "Common/CommonTypes.h"
|
||||||
|
@ -317,6 +319,9 @@ void GameFile::DoState(PointerWrap& p)
|
||||||
p.Do(m_disc_number);
|
p.Do(m_disc_number);
|
||||||
p.Do(m_apploader_date);
|
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_volume_banner.DoState(p);
|
||||||
m_custom_banner.DoState(p);
|
m_custom_banner.DoState(p);
|
||||||
m_default_cover.DoState(p);
|
m_default_cover.DoState(p);
|
||||||
|
@ -333,6 +338,58 @@ bool GameFile::IsElfOrDol() const
|
||||||
return name_end == ".elf" || name_end == ".dol";
|
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()
|
bool GameFile::WiiBannerChanged()
|
||||||
{
|
{
|
||||||
// Wii banners can only be read if there is a save file.
|
// Wii banners can only be read if there is a save file.
|
||||||
|
@ -389,7 +446,7 @@ bool GameFile::CustomBannerChanged()
|
||||||
std::string path, name;
|
std::string path, name;
|
||||||
SplitPath(m_file_path, &path, &name, nullptr);
|
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.
|
// for those who don't want to have a Homebrew Channel style folder structure.
|
||||||
if (!ReadPNGBanner(path + name + ".png"))
|
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& GameFile::GetName(const Core::TitleDatabase& title_database) const
|
||||||
{
|
{
|
||||||
const std::string& custom_name = title_database.GetTitleName(m_gametdb_id, GetConfigLanguage());
|
if (!m_custom_name.empty())
|
||||||
return custom_name.empty() ? GetName() : custom_name;
|
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();
|
const std::string& name = long_name ? GetLongName() : GetShortName();
|
||||||
if (!name.empty())
|
if (!name.empty())
|
||||||
return name;
|
return name;
|
||||||
|
@ -425,8 +488,11 @@ const std::string& GameFile::GetName(bool long_name) const
|
||||||
return m_file_name;
|
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();
|
const std::string& maker = long_maker ? GetLongMaker() : GetShortMaker();
|
||||||
if (!maker.empty())
|
if (!maker.empty())
|
||||||
return maker;
|
return maker;
|
||||||
|
@ -437,6 +503,14 @@ const std::string& GameFile::GetMaker(bool long_maker) const
|
||||||
return EMPTY_STRING;
|
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<DiscIO::Language> GameFile::GetLanguages() const
|
std::vector<DiscIO::Language> GameFile::GetLanguages() const
|
||||||
{
|
{
|
||||||
std::vector<DiscIO::Language> languages;
|
std::vector<DiscIO::Language> languages;
|
||||||
|
|
|
@ -52,8 +52,8 @@ public:
|
||||||
const std::string& GetFilePath() const { return m_file_path; }
|
const std::string& GetFilePath() const { return m_file_path; }
|
||||||
const std::string& GetFileName() const { return m_file_name; }
|
const std::string& GetFileName() const { return m_file_name; }
|
||||||
const std::string& GetName(const Core::TitleDatabase& title_database) const;
|
const std::string& GetName(const Core::TitleDatabase& title_database) const;
|
||||||
const std::string& GetName(bool long_name = true) const;
|
const std::string& GetName(bool allow_custom_name, bool long_name = true) const;
|
||||||
const std::string& GetMaker(bool long_maker = 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(DiscIO::Language l) const { return Lookup(l, m_short_names); }
|
||||||
const std::string& GetShortName() const { return LookupUsingConfigLanguage(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); }
|
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(DiscIO::Language l) const { return Lookup(l, m_long_makers); }
|
||||||
const std::string& GetLongMaker() const { return LookupUsingConfigLanguage(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(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<DiscIO::Language> GetLanguages() const;
|
std::vector<DiscIO::Language> GetLanguages() const;
|
||||||
const std::string& GetInternalName() const { return m_internal_name; }
|
const std::string& GetInternalName() const { return m_internal_name; }
|
||||||
const std::string& GetGameID() const { return m_game_id; }
|
const std::string& GetGameID() const { return m_game_id; }
|
||||||
|
@ -85,6 +85,8 @@ public:
|
||||||
const GameBanner& GetBannerImage() const;
|
const GameBanner& GetBannerImage() const;
|
||||||
const GameCover& GetCoverImage() const;
|
const GameCover& GetCoverImage() const;
|
||||||
void DoState(PointerWrap& p);
|
void DoState(PointerWrap& p);
|
||||||
|
bool XMLMetadataChanged();
|
||||||
|
void XMLMetadataCommit();
|
||||||
bool WiiBannerChanged();
|
bool WiiBannerChanged();
|
||||||
void WiiBannerCommit();
|
void WiiBannerCommit();
|
||||||
bool CustomBannerChanged();
|
bool CustomBannerChanged();
|
||||||
|
@ -102,6 +104,7 @@ private:
|
||||||
const std::string&
|
const std::string&
|
||||||
LookupUsingConfigLanguage(const std::map<DiscIO::Language, std::string>& strings) const;
|
LookupUsingConfigLanguage(const std::map<DiscIO::Language, std::string>& strings) const;
|
||||||
bool IsElfOrDol() const;
|
bool IsElfOrDol() const;
|
||||||
|
bool ReadXMLMetadata(const std::string& path);
|
||||||
bool ReadPNGBanner(const std::string& path);
|
bool ReadPNGBanner(const std::string& path);
|
||||||
|
|
||||||
// IMPORTANT: Nearly all data members must be save/restored in DoState.
|
// IMPORTANT: Nearly all data members must be save/restored in DoState.
|
||||||
|
@ -134,6 +137,9 @@ private:
|
||||||
u8 m_disc_number{};
|
u8 m_disc_number{};
|
||||||
std::string m_apploader_date;
|
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_volume_banner{};
|
||||||
GameBanner m_custom_banner{};
|
GameBanner m_custom_banner{};
|
||||||
GameCover m_default_cover{};
|
GameCover m_default_cover{};
|
||||||
|
@ -143,6 +149,9 @@ private:
|
||||||
// of GameFiles in a threadsafe way. They should not be handled in DoState.
|
// of GameFiles in a threadsafe way. They should not be handled in DoState.
|
||||||
struct
|
struct
|
||||||
{
|
{
|
||||||
|
std::string custom_name;
|
||||||
|
std::string custom_description;
|
||||||
|
std::string custom_maker;
|
||||||
GameBanner volume_banner;
|
GameBanner volume_banner;
|
||||||
GameBanner custom_banner;
|
GameBanner custom_banner;
|
||||||
GameCover default_cover;
|
GameCover default_cover;
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
|
|
||||||
namespace UICommon
|
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<std::string> FindAllGamePaths(const std::vector<std::string>& directories_to_scan,
|
std::vector<std::string> FindAllGamePaths(const std::vector<std::string>& directories_to_scan,
|
||||||
bool recursive_scan)
|
bool recursive_scan)
|
||||||
|
@ -166,6 +166,7 @@ bool GameFileCache::UpdateAdditionalMetadata(
|
||||||
|
|
||||||
bool GameFileCache::UpdateAdditionalMetadata(std::shared_ptr<GameFile>* game_file)
|
bool GameFileCache::UpdateAdditionalMetadata(std::shared_ptr<GameFile>* game_file)
|
||||||
{
|
{
|
||||||
|
const bool xml_metadata_changed = (*game_file)->XMLMetadataChanged();
|
||||||
const bool wii_banner_changed = (*game_file)->WiiBannerChanged();
|
const bool wii_banner_changed = (*game_file)->WiiBannerChanged();
|
||||||
const bool custom_banner_changed = (*game_file)->CustomBannerChanged();
|
const bool custom_banner_changed = (*game_file)->CustomBannerChanged();
|
||||||
|
|
||||||
|
@ -174,14 +175,18 @@ bool GameFileCache::UpdateAdditionalMetadata(std::shared_ptr<GameFile>* game_fil
|
||||||
const bool default_cover_changed = (*game_file)->DefaultCoverChanged();
|
const bool default_cover_changed = (*game_file)->DefaultCoverChanged();
|
||||||
const bool custom_cover_changed = (*game_file)->CustomCoverChanged();
|
const bool custom_cover_changed = (*game_file)->CustomCoverChanged();
|
||||||
|
|
||||||
if (!wii_banner_changed && !custom_banner_changed && !default_cover_changed &&
|
if (!xml_metadata_changed && !wii_banner_changed && !custom_banner_changed &&
|
||||||
!custom_cover_changed)
|
!default_cover_changed && !custom_cover_changed)
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// If a cached file needs an update, apply the updates to a copy and delete the original.
|
// 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.
|
// This makes the usage of cached files in other threads safe.
|
||||||
|
|
||||||
std::shared_ptr<GameFile> copy = std::make_shared<GameFile>(**game_file);
|
std::shared_ptr<GameFile> copy = std::make_shared<GameFile>(**game_file);
|
||||||
|
if (xml_metadata_changed)
|
||||||
|
copy->XMLMetadataCommit();
|
||||||
if (wii_banner_changed)
|
if (wii_banner_changed)
|
||||||
copy->WiiBannerCommit();
|
copy->WiiBannerCommit();
|
||||||
if (custom_banner_changed)
|
if (custom_banner_changed)
|
||||||
|
|
|
@ -48,6 +48,9 @@
|
||||||
<ProjectReference Include="$(ExternalsDir)discord-rpc\src\discord-rpc.vcxproj">
|
<ProjectReference Include="$(ExternalsDir)discord-rpc\src\discord-rpc.vcxproj">
|
||||||
<Project>{4482FD2A-EC43-3FFB-AC20-2E5C54B05EAD}</Project>
|
<Project>{4482FD2A-EC43-3FFB-AC20-2E5C54B05EAD}</Project>
|
||||||
</ProjectReference>
|
</ProjectReference>
|
||||||
|
<ProjectReference Include="$(ExternalsDir)pugixml\pugixml.vcxproj">
|
||||||
|
<Project>{38fee76f-f347-484b-949c-b4649381cffb}</Project>
|
||||||
|
</ProjectReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ClCompile Include="AutoUpdate.cpp" />
|
<ClCompile Include="AutoUpdate.cpp" />
|
||||||
|
|
Loading…
Reference in New Issue