Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GameFile: Support HBC-style XML metadata #8313

Merged
merged 2 commits into from Nov 9, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 4 additions & 2 deletions Source/Android/jni/GameList/GameFile.cpp
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion Source/Core/DolphinQt/Config/InfoWidget.cpp
Expand Up @@ -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) + " (" +
Expand Down
2 changes: 1 addition & 1 deletion Source/Core/DolphinQt/GameList/GameList.cpp
Expand Up @@ -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())
Expand Down
8 changes: 6 additions & 2 deletions Source/Core/DolphinQt/GameList/GameListModel.cpp
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions Source/Core/UICommon/CMakeLists.txt
Expand Up @@ -32,6 +32,7 @@ PUBLIC
common
cpp-optparse
minizip
pugixml

PRIVATE
$<$<BOOL:APPLE>:${IOK_LIBRARY}>
Expand Down
89 changes: 82 additions & 7 deletions Source/Core/UICommon/GameFile.cpp
Expand Up @@ -17,6 +17,8 @@
#include <utility>
#include <vector>

#include <pugixml.hpp>

#include "Common/ChunkFile.h"
#include "Common/CommonPaths.h"
#include "Common/CommonTypes.h"
Expand Down Expand Up @@ -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);
Expand All @@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do append the revision to the game name (altho this is a u16 and not just any plain string); and there would be the apploader date field (which is a string)...so we potentially could use those two.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would really not like to change the revision from u16 to std::string. I could do the apploader date, but an apploader date isn't really the same as a release date, and any custom data doesn't show up in the properties dialog anyway the way things are now (which is the only place where you can see the apploader date). Maybe we could rework this if we add a properties dialog for DOL/ELF files at some point.

// 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.
Expand Down Expand Up @@ -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"))
{
Expand All @@ -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;

Expand All @@ -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;
Expand Down
22 changes: 19 additions & 3 deletions Source/Core/UICommon/GameFile.h
Expand Up @@ -44,6 +44,13 @@ bool operator!=(const GameBanner& lhs, const GameBanner& rhs);
class GameFile final
{
public:
enum class Variant
{
LongAndPossiblyCustom,
LongAndNotCustom,
ShortAndNotCustom,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be the nittiest of nits, but...thoughts on using LongButNotCustom, ShortButNotCustom? :)
But then again, should it be LongOrPossiblyCustom for the first one?

You should probably ignore this comment for the sake of all the sheds we built so far.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using But there doesn't make too much sense to me, since there's no logical opposition between Long and Custom in LongButNotCustom. Also, it feels slightly awkward to make it inconsistent by using But for the last two ones but not the first one.

};

GameFile();
explicit GameFile(std::string path);
~GameFile();
Expand All @@ -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); }
Expand All @@ -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; }
Expand All @@ -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();
Expand All @@ -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.
Expand Down Expand Up @@ -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{};
Expand All @@ -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;
Expand Down
11 changes: 8 additions & 3 deletions Source/Core/UICommon/GameFileCache.cpp
Expand Up @@ -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)
Expand Down Expand Up @@ -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();

Expand All @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions Source/Core/UICommon/UICommon.vcxproj
Expand Up @@ -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" />
Expand Down