@@ -21,47 +21,15 @@ namespace Core
{
static const std::string EMPTY_STRING;

static std::string GetLanguageCode(DiscIO::Language language)
{
switch (language)
{
case DiscIO::Language::Japanese:
return "ja";
case DiscIO::Language::English:
return "en";
case DiscIO::Language::German:
return "de";
case DiscIO::Language::French:
return "fr";
case DiscIO::Language::Spanish:
return "es";
case DiscIO::Language::Italian:
return "it";
case DiscIO::Language::Dutch:
return "nl";
case DiscIO::Language::SimplifiedChinese:
return "zh_CN";
case DiscIO::Language::TraditionalChinese:
return "zh_TW";
case DiscIO::Language::Korean:
return "ko";
default:
return "en";
}
}

using Map = std::unordered_map<std::string, std::string>;

// Note that this function will not overwrite entries that already are in the maps
static bool LoadMap(const std::string& file_path, Map& map,
std::function<bool(const std::string& game_id)> predicate)
static Map LoadMap(const std::string& file_path)
{
Map map;

std::ifstream txt;
File::OpenFStream(txt, file_path, std::ios::in);

if (!txt.is_open())
return false;

std::string line;
while (std::getline(txt, line))
{
@@ -72,123 +40,94 @@ static bool LoadMap(const std::string& file_path, Map& map,
if (equals_index != std::string::npos)
{
const std::string game_id = StripSpaces(line.substr(0, equals_index));
if (game_id.length() >= 4 && predicate(game_id))
if (game_id.length() >= 4)
map.emplace(game_id, StripSpaces(line.substr(equals_index + 1)));
}
}
return true;
}

// This should only be used with the common game ID format (used by WiiTDBs), not Dolphin's.
// Otherwise, TurboGrafx-16 VC games (with the system ID P) will be misdetected as GameCube titles.
// The formats differ in that Dolphin's uses 6 characters for non-disc titles instead of 4.
static bool IsGCTitle(const std::string& game_id)
{
const char system_id = game_id[0];
return game_id.length() == 6 &&
(system_id == 'G' || system_id == 'D' || system_id == 'U' || system_id == 'P');
return map;
}

static bool IsWiiTitle(const std::string& game_id)
void TitleDatabase::AddLazyMap(DiscIO::Language language, const std::string& language_code)
{
// Assume that any non-GameCube title is a Wii title.
return !IsGCTitle(game_id);
m_title_maps[language] = [language_code]() -> Map {
return LoadMap(File::GetSysDirectory() + "wiitdb-" + language_code + ".txt");
};
}

static bool IsJapaneseGCTitle(const std::string& game_id)
TitleDatabase::TitleDatabase()
{
if (!IsGCTitle(game_id))
return false;
// User database
const std::string& load_directory = File::GetUserPath(D_LOAD_IDX);
m_user_title_map = LoadMap(load_directory + "wiitdb.txt");
if (m_user_title_map.empty())
m_user_title_map = LoadMap(load_directory + "titles.txt");

// Pre-defined databases (one per language)
AddLazyMap(DiscIO::Language::Japanese, "ja");
AddLazyMap(DiscIO::Language::English, "en");
AddLazyMap(DiscIO::Language::German, "de");
AddLazyMap(DiscIO::Language::French, "fr");
AddLazyMap(DiscIO::Language::Spanish, "es");
AddLazyMap(DiscIO::Language::Italian, "it");
AddLazyMap(DiscIO::Language::Dutch, "nl");
AddLazyMap(DiscIO::Language::SimplifiedChinese, "zh_CN");
AddLazyMap(DiscIO::Language::TraditionalChinese, "zh_TW");
AddLazyMap(DiscIO::Language::Korean, "ko");

// Titles that aren't part of the Wii TDB, but common enough to justify having entries for them.

return DiscIO::CountryCodeToCountry(game_id[3], DiscIO::Platform::GameCubeDisc) ==
DiscIO::Country::Japan;
// i18n: "Wii Menu" (or System Menu) refers to the Wii's main menu,
// which is (usually) the first thing users see when a Wii console starts.
m_base_map.emplace("0000000100000002", GetStringT("Wii Menu"));
for (const auto& id : {"HAXX", "00010001af1bf516"})
m_base_map.emplace(id, "The Homebrew Channel");
}

static bool IsNonJapaneseGCTitle(const std::string& game_id)
{
if (!IsGCTitle(game_id))
return false;

return DiscIO::CountryCodeToCountry(game_id[3], DiscIO::Platform::GameCubeDisc) !=
DiscIO::Country::Japan;
}
TitleDatabase::~TitleDatabase() = default;

// Note that this function will not overwrite entries that already are in the maps
static bool LoadMap(const std::string& file_path, Map& gc_map, Map& wii_map)
const std::string& TitleDatabase::GetTitleName(const std::string& gametdb_id,
DiscIO::Language language) const
{
Map map;
if (!LoadMap(file_path, map, [](const auto& game_id) { return true; }))
return false;
auto it = m_user_title_map.find(gametdb_id);
if (it != m_user_title_map.end())
return it->second;

for (auto& entry : map)
{
auto& destination_map = IsGCTitle(entry.first) ? gc_map : wii_map;
destination_map.emplace(std::move(entry));
}
return true;
}
if (!SConfig::GetInstance().m_use_builtin_title_database)
return EMPTY_STRING;

TitleDatabase::TitleDatabase()
{
// Load the user databases.
const std::string& load_directory = File::GetUserPath(D_LOAD_IDX);
if (!LoadMap(load_directory + "wiitdb.txt", m_gc_title_map, m_wii_title_map))
LoadMap(load_directory + "titles.txt", m_gc_title_map, m_wii_title_map);
const Map& map = *m_title_maps.at(language);
it = map.find(gametdb_id);
if (it != map.end())
return it->second;

if (!SConfig::GetInstance().m_use_builtin_title_database)
return;

// Load the database in the console language.
// Note: The GameCube language setting can't be set to Japanese,
// so instead, we use Japanese names iff the games are NTSC-J.
const std::string gc_code = GetLanguageCode(SConfig::GetInstance().GetCurrentLanguage(false));
const std::string wii_code = GetLanguageCode(SConfig::GetInstance().GetCurrentLanguage(true));
LoadMap(File::GetSysDirectory() + "wiitdb-ja.txt", m_gc_title_map, IsJapaneseGCTitle);
if (gc_code != "en")
if (language != DiscIO::Language::English)
{
LoadMap(File::GetSysDirectory() + "wiitdb-" + gc_code + ".txt", m_gc_title_map,
IsNonJapaneseGCTitle);
const Map& english_map = *m_title_maps.at(DiscIO::Language::English);
it = english_map.find(gametdb_id);
if (it != english_map.end())
return it->second;
}
if (wii_code != "en")
LoadMap(File::GetSysDirectory() + "wiitdb-" + wii_code + ".txt", m_wii_title_map, IsWiiTitle);

// Load the English database as the base database.
LoadMap(File::GetSysDirectory() + "wiitdb-en.txt", m_gc_title_map, m_wii_title_map);

// Titles that cannot be part of the Wii TDB,
// but common enough to justify having entries for them.
it = m_base_map.find(gametdb_id);
if (it != m_base_map.end())
return it->second;

// i18n: "Wii Menu" (or System Menu) refers to the Wii's main menu,
// which is (usually) the first thing users see when a Wii console starts.
m_wii_title_map.emplace("0000000100000002", GetStringT("Wii Menu"));
for (const auto& id : {"HAXX", "JODI", "00010001af1bf516", "LULZ", "OHBC"})
m_wii_title_map.emplace(id, "The Homebrew Channel");
}

TitleDatabase::~TitleDatabase() = default;

const std::string& TitleDatabase::GetTitleName(const std::string& game_id, TitleType type) const
{
const auto& map = IsWiiTitle(game_id) ? m_wii_title_map : m_gc_title_map;
const std::string key =
type == TitleType::Channel && game_id.length() == 6 ? game_id.substr(0, 4) : game_id;
const auto iterator = map.find(key);
return iterator != map.end() ? iterator->second : EMPTY_STRING;
return EMPTY_STRING;
}

const std::string& TitleDatabase::GetChannelName(u64 title_id) const
const std::string& TitleDatabase::GetChannelName(u64 title_id, DiscIO::Language language) const
{
const std::string id{
{static_cast<char>((title_id >> 24) & 0xff), static_cast<char>((title_id >> 16) & 0xff),
static_cast<char>((title_id >> 8) & 0xff), static_cast<char>(title_id & 0xff)}};
return GetTitleName(id, TitleType::Channel);
return GetTitleName(id, language);
}

std::string TitleDatabase::Describe(const std::string& game_id, TitleType type) const
std::string TitleDatabase::Describe(const std::string& gametdb_id, DiscIO::Language language) const
{
const std::string& title_name = GetTitleName(game_id, type);
const std::string& title_name = GetTitleName(gametdb_id, language);
if (title_name.empty())
return game_id;
return StringFromFormat("%s (%s)", title_name.c_str(), game_id.c_str());
return gametdb_id;
return StringFromFormat("%s (%s)", title_name.c_str(), gametdb_id.c_str());
}
} // namespace Core
@@ -8,6 +8,12 @@
#include <unordered_map>

#include "Common/CommonTypes.h"
#include "Common/Lazy.h"

namespace DiscIO
{
enum class Language;
}

namespace Core
{
@@ -18,24 +24,22 @@ class TitleDatabase final
TitleDatabase();
~TitleDatabase();

enum class TitleType
{
Channel,
Other,
};

// Get a user friendly title name for a game ID.
// Get a user friendly title name for a GameTDB ID.
// This falls back to returning an empty string if none could be found.
const std::string& GetTitleName(const std::string& game_id, TitleType = TitleType::Other) const;
const std::string& GetTitleName(const std::string& gametdb_id, DiscIO::Language language) const;

// Same as above, but takes a title ID instead of a game ID, and can only find names of channels.
const std::string& GetChannelName(u64 title_id) const;
// Same as above, but takes a title ID instead of a GameTDB ID, and only works for channels.
const std::string& GetChannelName(u64 title_id, DiscIO::Language language) const;

// Get a description for a game ID (title name if available + game ID).
std::string Describe(const std::string& game_id, TitleType = TitleType::Other) const;
// Get a description for a GameTDB ID (title name if available + GameTDB ID).
std::string Describe(const std::string& gametdb_id, DiscIO::Language language) const;

private:
std::unordered_map<std::string, std::string> m_wii_title_map;
std::unordered_map<std::string, std::string> m_gc_title_map;
void AddLazyMap(DiscIO::Language language, const std::string& language_code);

std::unordered_map<DiscIO::Language, Common::Lazy<std::unordered_map<std::string, std::string>>>
m_title_maps;
std::unordered_map<std::string, std::string> m_base_map;
std::unordered_map<std::string, std::string> m_user_title_map;
};
} // namespace Core
@@ -76,6 +76,7 @@ class Volume
return offset;
}
virtual std::string GetGameID(const Partition& partition = PARTITION_NONE) const = 0;
virtual std::string GetGameTDBID(const Partition& partition = PARTITION_NONE) const = 0;
virtual std::string GetMakerID(const Partition& partition = PARTITION_NONE) const = 0;
virtual std::optional<u16> GetRevision(const Partition& partition = PARTITION_NONE) const = 0;
virtual std::string GetInternalName(const Partition& partition = PARTITION_NONE) const = 0;
@@ -72,6 +72,17 @@ std::string VolumeGC::GetGameID(const Partition& partition) const
return DecodeString(id);
}

std::string VolumeGC::GetGameTDBID(const Partition& partition) const
{
const std::string game_id = GetGameID(partition);

// Don't return an ID for Datel discs that are using the game ID of NHL Hitz 2002
if (game_id == "GNHE5d" && !GetBootDOLOffset(*this, partition).has_value())
return "";

return game_id;
}

Region VolumeGC::GetRegion() const
{
const std::optional<u32> region_code = ReadSwapped<u32>(0x458, PARTITION_NONE);
@@ -34,6 +34,7 @@ class VolumeGC : public Volume
const Partition& partition = PARTITION_NONE) const override;
const FileSystem* GetFileSystem(const Partition& partition = PARTITION_NONE) const override;
std::string GetGameID(const Partition& partition = PARTITION_NONE) const override;
std::string GetGameTDBID(const Partition& partition = PARTITION_NONE) const override;
std::string GetMakerID(const Partition& partition = PARTITION_NONE) const override;
std::optional<u16> GetRevision(const Partition& partition = PARTITION_NONE) const override;
std::string GetInternalName(const Partition& partition = PARTITION_NONE) const override;
@@ -105,6 +105,11 @@ std::string VolumeWAD::GetGameID(const Partition& partition) const
return m_tmd.GetGameID();
}

std::string VolumeWAD::GetGameTDBID(const Partition& partition) const
{
return m_tmd.GetGameTDBID();
}

std::string VolumeWAD::GetMakerID(const Partition& partition) const
{
char temp[2];
@@ -35,6 +35,7 @@ class VolumeWAD : public Volume
std::optional<u64> GetTitleID(const Partition& partition = PARTITION_NONE) const override;
const IOS::ES::TMDReader& GetTMD(const Partition& partition = PARTITION_NONE) const override;
std::string GetGameID(const Partition& partition = PARTITION_NONE) const override;
std::string GetGameTDBID(const Partition& partition = PARTITION_NONE) const override;
std::string GetMakerID(const Partition& partition = PARTITION_NONE) const override;
std::optional<u16> GetRevision(const Partition& partition = PARTITION_NONE) const override;
std::string GetInternalName(const Partition& partition = PARTITION_NONE) const override
@@ -278,6 +278,11 @@ std::string VolumeWii::GetGameID(const Partition& partition) const
return DecodeString(id);
}

std::string VolumeWii::GetGameTDBID(const Partition& partition) const
{
return GetGameID(partition);
}

Region VolumeWii::GetRegion() const
{
const std::optional<u32> region_code = m_reader->ReadSwapped<u32>(0x4E000);
@@ -45,6 +45,7 @@ class VolumeWii : public Volume
u64 partition_data_offset);
u64 PartitionOffsetToRawOffset(u64 offset, const Partition& partition) const override;
std::string GetGameID(const Partition& partition) const override;
std::string GetGameTDBID(const Partition& partition) const override;
std::string GetMakerID(const Partition& partition) const override;
std::optional<u16> GetRevision(const Partition& partition) const override;
std::string GetInternalName(const Partition& partition) const override;
@@ -26,8 +26,8 @@
#include "UICommon/GameFile.h"

GeckoCodeWidget::GeckoCodeWidget(const UICommon::GameFile& game, bool restart_required)
: m_game(game), m_game_id(game.GetGameID()), m_game_revision(game.GetRevision()),
m_restart_required(restart_required)
: m_game(game), m_game_id(game.GetGameID()), m_gametdb_id(game.GetGameTDBID()),
m_game_revision(game.GetRevision()), m_restart_required(restart_required)
{
CreateWidgets();
ConnectWidgets();
@@ -251,7 +251,7 @@ void GeckoCodeWidget::DownloadCodes()
{
bool success;

std::vector<Gecko::GeckoCode> codes = Gecko::DownloadCodes(m_game_id, &success);
std::vector<Gecko::GeckoCode> codes = Gecko::DownloadCodes(m_gametdb_id, &success);

if (!success)
{
@@ -48,6 +48,7 @@ class GeckoCodeWidget : public QWidget

const UICommon::GameFile& m_game;
std::string m_game_id;
std::string m_gametdb_id;
u16 m_game_revision;

CheatWarningWidget* m_warning;
@@ -43,6 +43,7 @@
#include "Core/TitleDatabase.h"
#include "Core/WiiUtils.h"

#include "DiscIO/Enums.h"
#include "DiscIO/NANDImporter.h"
#include "DiscIO/WiiSaveBanner.h"

@@ -1038,11 +1039,12 @@ void MenuBar::CheckNAND()
{
std::string title_listings;
Core::TitleDatabase title_db;
const DiscIO::Language language = SConfig::GetInstance().GetCurrentLanguage(true);
for (const u64 title_id : result.titles_to_remove)
{
title_listings += StringFromFormat("%016" PRIx64, title_id);

const std::string database_name = title_db.GetChannelName(title_id);
const std::string database_name = title_db.GetChannelName(title_id, language);
if (!database_name.empty())
{
title_listings += " - " + database_name;
@@ -50,14 +50,25 @@ static const std::string EMPTY_STRING;
static bool UseGameCovers()
{
// We ifdef this out on Android because accessing the config before emulation start makes us crash.
// The Android GUI doesn't support covers anyway, so this doesn't make us lose out on functionality.
// The Android GUI handles covers in Java anyway, so this doesn't make us lose any functionality.
#ifdef ANDROID
return false;
#else
return Config::Get(Config::MAIN_USE_GAME_COVERS);
#endif
}

DiscIO::Language GameFile::GetConfigLanguage() const
{
#ifdef ANDROID
// TODO: Make the Android app load the config at app start instead of emulation start
// so that we can access the user's preference here
return DiscIO::Language::English;
#else
return SConfig::GetInstance().GetCurrentLanguage(DiscIO::IsWii(m_platform));
#endif
}

bool operator==(const GameBanner& lhs, const GameBanner& rhs)
{
return std::tie(lhs.buffer, lhs.width, lhs.height) == std::tie(rhs.buffer, rhs.width, rhs.height);
@@ -94,15 +105,7 @@ const std::string& GameFile::Lookup(DiscIO::Language language,
const std::string&
GameFile::LookupUsingConfigLanguage(const std::map<DiscIO::Language, std::string>& strings) const
{
#ifdef ANDROID
// TODO: Make the Android app load the config at app start instead of emulation start
// so that we can access the user's preference here
const DiscIO::Language language = DiscIO::Language::English;
#else
const bool wii = DiscIO::IsWii(m_platform);
const DiscIO::Language language = SConfig::GetInstance().GetCurrentLanguage(wii);
#endif
return Lookup(language, strings);
return Lookup(GetConfigLanguage(), strings);
}

GameFile::GameFile(const std::string& path)
@@ -132,6 +135,7 @@ GameFile::GameFile(const std::string& path)

m_internal_name = volume->GetInternalName();
m_game_id = volume->GetGameID();
m_gametdb_id = volume->GetGameTDBID();
m_title_id = volume->GetTitleID().value_or(0);
m_maker_id = volume->GetMakerID();
m_revision = volume->GetRevision().value_or(0);
@@ -198,9 +202,8 @@ void GameFile::DownloadDefaultCover()

const auto cover_path = File::GetUserPath(D_COVERCACHE_IDX) + DIR_SEP;

// If covers have already been downloaded, abort
if (File::Exists(cover_path + m_game_id + ".png") ||
File::Exists(cover_path + m_game_id.substr(0, 4) + ".png"))
// If the cover has already been downloaded, abort
if (File::Exists(cover_path + m_gametdb_id + ".png"))
return;

Common::HttpRequest request;
@@ -249,22 +252,13 @@ void GameFile::DownloadDefaultCover()
break;
}

auto response = request.Get(StringFromFormat(COVER_URL, region_code.c_str(), m_game_id.c_str()));
auto response =
request.Get(StringFromFormat(COVER_URL, region_code.c_str(), m_gametdb_id.c_str()));

if (response)
{
File::WriteStringToFile(std::string(response.value().begin(), response.value().end()),
cover_path + m_game_id + ".png");
return;
}

response =
request.Get(StringFromFormat(COVER_URL, region_code.c_str(), m_game_id.substr(0, 4).c_str()));

if (response)
{
File::WriteStringToFile(std::string(response.value().begin(), response.value().end()),
cover_path + m_game_id.substr(0, 4) + ".png");
cover_path + m_gametdb_id + ".png");
}
}

@@ -277,10 +271,7 @@ bool GameFile::DefaultCoverChanged()

std::string contents;

File::ReadFileToString(cover_path + m_game_id + ".png", contents);

if (contents.empty())
File::ReadFileToString(cover_path + m_game_id.substr(0, 4).c_str() + ".png", contents);
File::ReadFileToString(cover_path + m_gametdb_id + ".png", contents);

if (contents.empty())
return false;
@@ -328,6 +319,7 @@ void GameFile::DoState(PointerWrap& p)
p.Do(m_descriptions);
p.Do(m_internal_name);
p.Do(m_game_id);
p.Do(m_gametdb_id);
p.Do(m_title_id);
p.Do(m_maker_id);

@@ -433,10 +425,7 @@ void GameFile::CustomBannerCommit()

const std::string& GameFile::GetName(const Core::TitleDatabase& title_database) const
{
const auto type = m_platform == DiscIO::Platform::WiiWAD ?
Core::TitleDatabase::TitleType::Channel :
Core::TitleDatabase::TitleType::Other;
const std::string& custom_name = title_database.GetTitleName(m_game_id, type);
const std::string& custom_name = title_database.GetTitleName(m_gametdb_id, GetConfigLanguage());
return custom_name.empty() ? GetName() : custom_name;
}

@@ -67,6 +67,7 @@ class GameFile final
std::vector<DiscIO::Language> GetLanguages() const;
const std::string& GetInternalName() const { return m_internal_name; }
const std::string& GetGameID() const { return m_game_id; }
const std::string& GetGameTDBID() const { return m_gametdb_id; }
u64 GetTitleID() const { return m_title_id; }
const std::string& GetMakerID() const { return m_maker_id; }
u16 GetRevision() const { return m_revision; }
@@ -95,6 +96,7 @@ class GameFile final
void CustomCoverCommit();

private:
DiscIO::Language GetConfigLanguage() const;
static const std::string& Lookup(DiscIO::Language language,
const std::map<DiscIO::Language, std::string>& strings);
const std::string&
@@ -120,6 +122,7 @@ class GameFile final
std::map<DiscIO::Language, std::string> m_descriptions{};
std::string m_internal_name{};
std::string m_game_id{};
std::string m_gametdb_id{};
u64 m_title_id{};
std::string m_maker_id{};

@@ -27,7 +27,7 @@

namespace UICommon
{
static constexpr u32 CACHE_REVISION = 14; // Last changed in PR 7441
static constexpr u32 CACHE_REVISION = 15; // Last changed in PR 7816

std::vector<std::string> FindAllGamePaths(const std::vector<std::string>& directories_to_scan,
bool recursive_scan)