Skip to content
Permalink
Browse files

Merge pull request #8313 from JosJuice/gamelist-xml

GameFile: Support HBC-style XML metadata
  • Loading branch information
leoetlino committed Nov 9, 2019
2 parents 37f1c95 + d8958fb commit 08b191ee8e4ec0de2fca2054f678df816e0b1db1
@@ -84,13 +84,15 @@ 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(UICommon::GameFile::Variant::LongAndPossiblyCustom));
}

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(UICommon::GameFile::Variant::LongAndPossiblyCustom));
}

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));

const std::string game_maker = m_game.GetMaker();
const std::string game_maker = m_game.GetMaker(UICommon::GameFile::Variant::LongAndNotCustom);

QLineEdit* maker =
CreateValueDisplay((game_maker.empty() ? UNKNOWN_NAME.toStdString() : game_maker) + " (" +
@@ -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(UICommon::GameFile::Variant::LongAndPossiblyCustom));
}

if (!failed.isEmpty())
@@ -124,13 +124,17 @@ 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(UICommon::GameFile::Variant::LongAndPossiblyCustom))
.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(UICommon::GameFile::Variant::LongAndPossiblyCustom));
}
break;
case COL_FILE_NAME:
if (role == Qt::DisplayRole || role == Qt::InitialSortOrderRole)
@@ -32,6 +32,7 @@ PUBLIC
common
cpp-optparse
minizip
pugixml

PRIVATE
$<$<BOOL:APPLE>:${IOK_LIBRARY}>
@@ -17,6 +17,8 @@
#include <utility>
#include <vector>

#include <pugixml.hpp>

#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,23 +468,33 @@ 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(Variant::LongAndPossiblyCustom) : database_name;
}

const std::string& GameFile::GetName(bool long_name) const
const std::string& GameFile::GetName(Variant variant) const
{
const std::string& name = long_name ? GetLongName() : GetShortName();
if (variant == Variant::LongAndPossiblyCustom && !m_custom_name.empty())
return m_custom_name;

const std::string& name = variant == Variant::ShortAndNotCustom ? GetShortName() : GetLongName();
if (!name.empty())
return name;

// No usable name, return filename (better than nothing)
return m_file_name;
}

const std::string& GameFile::GetMaker(bool long_maker) const
const std::string& GameFile::GetMaker(Variant variant) const
{
const std::string& maker = long_maker ? GetLongMaker() : GetShortMaker();
if (variant == Variant::LongAndPossiblyCustom && !m_custom_maker.empty())
return m_custom_maker;

const std::string& maker =
variant == Variant::ShortAndNotCustom ? GetShortMaker() : GetLongMaker();
if (!maker.empty())
return maker;

@@ -437,6 +504,14 @@ const std::string& GameFile::GetMaker(bool long_maker) const
return EMPTY_STRING;
}

const std::string& GameFile::GetDescription(Variant variant) const
{
if (variant == Variant::LongAndPossiblyCustom && !m_custom_description.empty())
return m_custom_description;

return LookupUsingConfigLanguage(m_descriptions);
}

std::vector<DiscIO::Language> GameFile::GetLanguages() const
{
std::vector<DiscIO::Language> languages;
@@ -44,6 +44,13 @@ bool operator!=(const GameBanner& lhs, const GameBanner& rhs);
class GameFile final
{
public:
enum class Variant
{
LongAndPossiblyCustom,
LongAndNotCustom,
ShortAndNotCustom,
};

GameFile();
explicit GameFile(std::string path);
~GameFile();
@@ -52,8 +59,8 @@ class GameFile final
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(Variant variant) const;
const std::string& GetMaker(Variant variant) 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 +70,7 @@ class GameFile final
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(Variant variant) const;
std::vector<DiscIO::Language> GetLanguages() const;
const std::string& GetInternalName() const { return m_internal_name; }
const std::string& GetGameID() const { return m_game_id; }
@@ -85,6 +92,8 @@ class GameFile final
const GameBanner& GetBannerImage() const;
const GameCover& GetCoverImage() const;
void DoState(PointerWrap& p);
bool XMLMetadataChanged();
void XMLMetadataCommit();
bool WiiBannerChanged();
void WiiBannerCommit();
bool CustomBannerChanged();
@@ -102,6 +111,7 @@ class GameFile final
const std::string&
LookupUsingConfigLanguage(const std::map<DiscIO::Language, std::string>& 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 +144,9 @@ class GameFile final
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 +156,9 @@ class GameFile final
// 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;
@@ -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<std::string> FindAllGamePaths(const std::vector<std::string>& directories_to_scan,
bool recursive_scan)
@@ -166,6 +166,7 @@ bool GameFileCache::UpdateAdditionalMetadata(

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 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 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<GameFile> copy = std::make_shared<GameFile>(**game_file);
if (xml_metadata_changed)
copy->XMLMetadataCommit();
if (wii_banner_changed)
copy->WiiBannerCommit();
if (custom_banner_changed)
@@ -48,6 +48,9 @@
<ProjectReference Include="$(ExternalsDir)discord-rpc\src\discord-rpc.vcxproj">
<Project>{4482FD2A-EC43-3FFB-AC20-2E5C54B05EAD}</Project>
</ProjectReference>
<ProjectReference Include="$(ExternalsDir)pugixml\pugixml.vcxproj">
<Project>{38fee76f-f347-484b-949c-b4649381cffb}</Project>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<ClCompile Include="AutoUpdate.cpp" />

0 comments on commit 08b191e

Please sign in to comment.
You can’t perform that action at this time.