Qt: Use localized game icons and titles

This commit is contained in:
Megamouse 2025-03-15 14:00:29 +01:00
parent a1fa8dd701
commit 42ba0b6271
11 changed files with 197 additions and 49 deletions

View file

@ -1382,9 +1382,11 @@ error_code sceNpTrophyGetGameIcon(u32 context, u32 handle, vm::ptr<void> buffer,
return SCE_NP_TROPHY_ERROR_INVALID_ARGUMENT;
}
fs::file icon_file(vfs::get("/dev_hdd0/home/" + Emu.GetUsr() + "/trophy/" + ctxt->trp_name + "/ICON0.PNG"));
// Try to get icon in current language first
const std::string trophy_path = fmt::format("/dev_hdd0/home/%s/trophy/%s/", Emu.GetUsr(), ctxt->trp_name);
fs::file icon_file(vfs::get(fmt::format("%s/ICON0_%02d.PNG", trophy_path, static_cast<s32>(g_cfg.sys.language))));
if (!icon_file)
if (!icon_file && !icon_file.open(vfs::get(fmt::format("%s/ICON0.PNG", trophy_path))))
{
return SCE_NP_TROPHY_ERROR_UNKNOWN_FILE;
}

View file

@ -771,25 +771,40 @@ std::string Emulator::GetBackgroundPicturePath() const
}
constexpr auto search_barrier = "barrier";
const std::string locale_suffix = fmt::format("_%02d", static_cast<s32>(g_cfg.sys.language.get()));
std::initializer_list<std::string> testees =
{
m_sfo_dir + fmt::format("/PIC0%s.PNG", locale_suffix),
m_sfo_dir + fmt::format("/PIC1%s.PNG", locale_suffix),
m_sfo_dir + fmt::format("/PIC2%s.PNG", locale_suffix),
m_sfo_dir + fmt::format("/PIC3%s.PNG", locale_suffix),
search_barrier,
m_sfo_dir + "/PIC0.PNG",
m_sfo_dir + "/PIC1.PNG",
m_sfo_dir + "/PIC2.PNG",
m_sfo_dir + "/PIC3.PNG",
search_barrier,
!disc_dir.empty() ? (disc_dir + fmt::format("/PIC0%s.PNG", locale_suffix)) : disc_dir,
!disc_dir.empty() ? (disc_dir + fmt::format("/PIC1%s.PNG", locale_suffix)) : disc_dir,
!disc_dir.empty() ? (disc_dir + fmt::format("/PIC2%s.PNG", locale_suffix)) : disc_dir,
!disc_dir.empty() ? (disc_dir + fmt::format("/PIC3%s.PNG", locale_suffix)) : disc_dir,
search_barrier,
!disc_dir.empty() ? (disc_dir + "/PIC0.PNG") : disc_dir,
!disc_dir.empty() ? (disc_dir + "/PIC1.PNG") : disc_dir,
!disc_dir.empty() ? (disc_dir + "/PIC2.PNG") : disc_dir,
!disc_dir.empty() ? (disc_dir + "/PIC3.PNG") : disc_dir,
search_barrier,
m_sfo_dir + fmt::format("/ICON0%s.PNG", locale_suffix),
search_barrier,
m_sfo_dir + "/ICON0.PNG",
search_barrier,
!disc_dir.empty() ? (disc_dir + fmt::format("/ICON0%s.PNG", locale_suffix)) : disc_dir,
search_barrier,
!disc_dir.empty() ? (disc_dir + "/ICON0.PNG") : disc_dir,
};
// Try to return the picture with the highest resultion
// Try to return the picture with the highest resolution
// Be naive and assume that its the one that spans over the most bytes
usz max_file_size = 0;
usz index_of_largest_file = umax;
@ -1600,6 +1615,10 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch,
g_backup_cfg.from_string(g_cfg.to_string());
}
// Get localized title
m_localized_title = std::string(psf::get_string(_psf, fmt::format("TITLE_%02d", static_cast<s32>(g_cfg.sys.language.get())), m_title));
sys_log.notice("Localized Title: %s", GetLocalizedTitle());
// Set RTM usage
g_use_rtm = utils::has_rtm() && (((utils::has_mpx() && !utils::has_tsx_force_abort()) && g_cfg.core.enable_TSX == tsx_usage::enabled) || g_cfg.core.enable_TSX == tsx_usage::forced);
@ -2176,15 +2195,19 @@ game_boot_result Emulator::Load(const std::string& title_id, bool is_disc_patch,
{
sys_log.notice("Title was set from %s to %s", m_title, bdvd_title);
m_title = bdvd_title;
const auto localized_title = psf::get_string(disc_psf_obj, fmt::format("TITLE_%02d", static_cast<s32>(g_cfg.sys.language.get())), m_title);
if (m_localized_title != localized_title)
{
sys_log.notice("Localized Title was set from %s to %s", m_localized_title, localized_title);
m_localized_title = std::move(localized_title);
}
}
}
for (auto& c : m_title)
{
// Replace newlines with spaces
if (c == '\n')
c = ' ';
}
// Replace newlines with spaces
std::replace(m_title.begin(), m_title.end(), '\n', ' ');
std::replace(m_localized_title.begin(), m_localized_title.end(), '\n', ' ');
// Mount /host_root/ if necessary (special value)
if (g_cfg.vfs.host_root)
@ -3899,7 +3922,7 @@ std::string Emulator::GetFormattedTitle(double fps) const
{
rpcs3::title_format_data title_data;
title_data.format = g_cfg.misc.title_format.to_string();
title_data.title = GetTitle();
title_data.title = GetLocalizedTitle();
title_data.title_id = GetTitleID();
title_data.renderer = g_cfg.video.renderer.to_string();
title_data.vulkan_adapter = g_cfg.video.vk.adapter.to_string();

View file

@ -141,6 +141,7 @@ class Emulator final
std::string m_path_original;
std::string m_title_id;
std::string m_title;
std::string m_localized_title;
std::string m_app_version;
std::string m_hash;
std::string m_cat;
@ -279,6 +280,11 @@ public:
return m_title;
}
const std::string& GetLocalizedTitle() const
{
return m_localized_title;
}
const std::string GetTitleAndTitleID() const
{
return m_title + (m_title_id.empty() ? "" : " [" + m_title_id + "]");

View file

@ -321,7 +321,7 @@ compat::package_info game_compatibility::GetPkgInfo(const QString& pkg_path, gam
{
if (const std::string localized_title = package.get_title(title_key); !localized_title.empty())
{
info.title= qstr(localized_title);
info.title = qstr(localized_title);
}
if (const std::string localized_changelog = package.get_changelog(changelog_key); !localized_changelog.empty())

View file

@ -8,6 +8,7 @@
#include "persistent_settings.h"
#include "emu_settings.h"
#include "gui_settings.h"
#include "gui_application.h"
#include "game_list_table.h"
#include "game_list_grid.h"
#include "game_list_grid_item.h"
@ -573,9 +574,13 @@ void game_list_frame::OnParsingFinished()
sort(m_path_entries.begin(), m_path_entries.end(), [](const path_entry& l, const path_entry& r){return l.path < r.path;});
m_path_entries.erase(unique(m_path_entries.begin(), m_path_entries.end(), [](const path_entry& l, const path_entry& r){return l.path == r.path;}), m_path_entries.end());
const s32 language_index = gui_application::get_language_id();
const std::string game_icon_path = fs::get_config_dir() + "/Icons/game_icons/";
const std::string localized_title = fmt::format("TITLE_%02d", language_index);
const std::string localized_icon = fmt::format("ICON0_%02d.PNG", language_index);
const std::string localized_movie = fmt::format("ICON1_%02d.PAM", language_index);
const auto add_game = [this, dev_flash, cat_unknown_localized = localized.category.unknown.toStdString(), cat_unknown = cat::cat_unknown.toStdString(), game_icon_path, _hdd, play_hover_movies = m_play_hover_movies, show_custom_icons = m_show_custom_icons](const std::string& dir_or_elf)
const auto add_game = [this, localized_title, localized_icon, localized_movie, dev_flash, cat_unknown_localized = localized.category.unknown.toStdString(), cat_unknown = cat::cat_unknown.toStdString(), game_icon_path, _hdd, play_hover_movies = m_play_hover_movies, show_custom_icons = m_show_custom_icons](const std::string& dir_or_elf)
{
GameInfo game{};
game.path = dir_or_elf;
@ -624,8 +629,11 @@ void game_list_frame::OnParsingFinished()
}
else
{
std::string_view name = psf::get_string(psf, localized_title);
if (name.empty()) name = psf::get_string(psf, "TITLE", cat_unknown_localized);
game.serial = std::string(title_id);
game.name = std::string(psf::get_string(psf, "TITLE", cat_unknown_localized));
game.name = std::string(name);
game.app_ver = std::string(psf::get_string(psf, "APP_VER", cat_unknown_localized));
game.version = std::string(psf::get_string(psf, "VERSION", cat_unknown_localized));
game.category = std::string(psf::get_string(psf, "CATEGORY", cat_unknown));
@ -635,23 +643,6 @@ void game_list_frame::OnParsingFinished()
game.sound_format = psf::get_integer(psf, "SOUND_FORMAT", 0);
game.bootable = psf::get_integer(psf, "BOOTABLE", 0);
game.attr = psf::get_integer(psf, "ATTRIBUTE", 0);
game.icon_path = sfo_dir + "/ICON0.PNG";
game.movie_path = sfo_dir + "/ICON1.PAM";
if (game.category == "DG")
{
const std::string game_data_dir = _hdd + "game/" + game.serial;
if (std::string latest_icon = game_data_dir + "/ICON0.PNG"; fs::is_file(latest_icon))
{
game.icon_path = std::move(latest_icon);
}
if (std::string latest_movie = game_data_dir + "/ICON1.PAM"; fs::is_file(latest_movie))
{
game.movie_path = std::move(latest_movie);
}
}
}
if (show_custom_icons)
@ -662,6 +653,30 @@ void game_list_frame::OnParsingFinished()
}
}
if (game.icon_path.empty())
{
if (std::string icon_path = sfo_dir + "/" + localized_icon; fs::is_file(icon_path))
{
game.icon_path = std::move(icon_path);
}
else
{
game.icon_path = sfo_dir + "/ICON0.PNG";
}
}
if (game.movie_path.empty())
{
if (std::string movie_path = sfo_dir + "/" + localized_movie; fs::is_file(movie_path))
{
game.movie_path = std::move(movie_path);
}
else if (std::string movie_path = sfo_dir + "/ICON1.PAM"; fs::is_file(movie_path))
{
game.movie_path = std::move(movie_path);
}
}
const QString serial = qstr(game.serial);
m_games_mutex.lock();
@ -720,13 +735,7 @@ void game_list_frame::OnParsingFinished()
info.hasCustomConfig = fs::is_file(rpcs3::utils::get_custom_config_path(info.info.serial));
info.hasCustomPadConfig = fs::is_file(rpcs3::utils::get_custom_input_config_path(info.info.serial));
info.has_hover_gif = fs::is_file(game_icon_path + info.info.serial + "/hover.gif");
info.has_hover_pam = fs::is_file(info.info.movie_path);
// Free some memory
if (!info.has_hover_pam)
{
info.info.movie_path.clear();
}
info.has_hover_pam = !info.info.movie_path.empty();
m_games.push(std::make_shared<gui_game_info>(std::move(info)));
};
@ -837,14 +846,21 @@ void game_list_frame::OnRefreshFinished()
const Localized localized;
const std::string cat_unknown_localized = localized.category.unknown.toStdString();
const s32 language_index = gui_application::get_language_id();
const std::string localized_icon = fmt::format("ICON0_%02d.PNG", language_index);
const std::string localized_movie = fmt::format("ICON1_%02d.PAM", language_index);
// Try to update the app version for disc games if there is a patch
// Also try to find updated game icons and movies
for (const game_info& entry : m_game_data)
{
if (entry->info.category != "DG") continue;
for (const auto& other : m_game_data)
{
if (other->info.category == "DG") continue;
if (entry->info.serial != other->info.serial) continue;
// The patch is game data and must have the same serial and an app version
static constexpr auto version_is_bigger = [](const std::string& v0, const std::string& v1, const std::string& serial, bool is_fw)
{
@ -861,7 +877,7 @@ void game_list_frame::OnRefreshFinished()
return false;
};
if (entry->info.serial == other->info.serial && other->info.category != "DG" && other->info.app_ver != cat_unknown_localized)
if (other->info.app_ver != cat_unknown_localized)
{
// Update the app version if it's higher than the disc's version (old games may not have an app version)
if (entry->info.app_ver == cat_unknown_localized || version_is_bigger(other->info.app_ver, entry->info.app_ver, entry->info.serial, true))
@ -879,6 +895,26 @@ void game_list_frame::OnRefreshFinished()
entry->info.parental_lvl = other->info.parental_lvl;
}
}
if (std::string icon_path = other->info.path + "/" + localized_icon; fs::is_file(icon_path))
{
entry->info.icon_path = std::move(icon_path);
}
else if (std::string icon_path = other->info.path + "/ICON0.PNG"; fs::is_file(icon_path))
{
entry->info.icon_path = std::move(icon_path);
}
if (std::string movie_path = other->info.path + "/" + localized_movie; fs::is_file(movie_path))
{
entry->info.movie_path = std::move(movie_path);
entry->has_hover_pam = true;
}
else if (std::string movie_path = other->info.path + "/ICON1.PAM"; fs::is_file(movie_path))
{
entry->info.movie_path = std::move(movie_path);
entry->has_hover_pam = true;
}
}
}

View file

@ -21,6 +21,7 @@
#endif
#include "Emu/Audio/audio_utils.h"
#include "Emu/Cell/Modules/cellSysutil.h"
#include "Emu/Io/Null/null_camera_handler.h"
#include "Emu/Io/Null/null_music_handler.h"
#include "Emu/vfs_config.h"
@ -66,6 +67,8 @@ LOG_CHANNEL(gui_log, "GUI");
std::unique_ptr<raw_mouse_handler> g_raw_mouse_handler;
s32 gui_application::m_language_id = static_cast<s32>(CELL_SYSUTIL_LANG_ENGLISH_US);
[[noreturn]] void report_fatal_error(std::string_view text, bool is_html = false, bool include_help_text = true);
gui_application::gui_application(int& argc, char** argv) : QApplication(argc, argv)
@ -228,13 +231,13 @@ void gui_application::SwitchTranslator(QTranslator& translator, const QString& f
installTranslator(&translator);
}
}
else if (const QString default_code = QLocale(QLocale::English).bcp47Name(); language_code != default_code)
else if (QString default_code = QLocale(QLocale::English).bcp47Name(); language_code != default_code)
{
// show error, but ignore default case "en", since it is handled in source code
gui_log.error("No translation file found in: %s", file_path);
// reset current language to default "en"
m_language_code = default_code;
set_language_code(std::move(default_code));
}
}
@ -245,7 +248,7 @@ void gui_application::LoadLanguage(const QString& language_code)
return;
}
m_language_code = language_code;
set_language_code(language_code);
const QLocale locale = QLocale(language_code);
const QString locale_name = QLocale::languageToString(locale.language());
@ -307,6 +310,69 @@ QStringList gui_application::GetAvailableLanguageCodes()
return language_codes;
}
void gui_application::set_language_code(QString language_code)
{
m_language_code = language_code;
// Transform language code to lowercase and use '-'
language_code = language_code.toLower().replace("_", "-");
// Try to find the CELL language ID for this language code
static const std::map<QString, CellSysutilLang> language_ids = {
{"ja", CELL_SYSUTIL_LANG_JAPANESE },
{"en", CELL_SYSUTIL_LANG_ENGLISH_US },
{"en-us", CELL_SYSUTIL_LANG_ENGLISH_US },
{"en-gb", CELL_SYSUTIL_LANG_ENGLISH_GB },
{"fr", CELL_SYSUTIL_LANG_FRENCH },
{"es", CELL_SYSUTIL_LANG_SPANISH },
{"de", CELL_SYSUTIL_LANG_GERMAN },
{"it", CELL_SYSUTIL_LANG_ITALIAN },
{"nl", CELL_SYSUTIL_LANG_DUTCH },
{"pt", CELL_SYSUTIL_LANG_PORTUGUESE_PT },
{"pt-pt", CELL_SYSUTIL_LANG_PORTUGUESE_PT },
{"pt-br", CELL_SYSUTIL_LANG_PORTUGUESE_BR },
{"ru", CELL_SYSUTIL_LANG_RUSSIAN },
{"ko", CELL_SYSUTIL_LANG_KOREAN },
{"zh", CELL_SYSUTIL_LANG_CHINESE_T },
{"zh-hant", CELL_SYSUTIL_LANG_CHINESE_T },
{"zh-hans", CELL_SYSUTIL_LANG_CHINESE_S },
{"fi", CELL_SYSUTIL_LANG_FINNISH },
{"sv", CELL_SYSUTIL_LANG_SWEDISH },
{"da", CELL_SYSUTIL_LANG_DANISH },
{"no", CELL_SYSUTIL_LANG_NORWEGIAN },
{"nn", CELL_SYSUTIL_LANG_NORWEGIAN },
{"nb", CELL_SYSUTIL_LANG_NORWEGIAN },
{"pl", CELL_SYSUTIL_LANG_POLISH },
{"tr", CELL_SYSUTIL_LANG_TURKISH },
};
// Check direct match first
const auto it = language_ids.find(language_code);
if (it != language_ids.cend())
{
m_language_id = static_cast<s32>(it->second);
return;
}
// Try to find closest match
for (const auto& [code, id] : language_ids)
{
if (language_code.startsWith(code))
{
m_language_id = static_cast<s32>(id);
return;
}
}
// Fallback to English (US)
m_language_id = static_cast<s32>(CELL_SYSUTIL_LANG_ENGLISH_US);
}
s32 gui_application::get_language_id()
{
return m_language_id;
}
void gui_application::InitializeConnects()
{
connect(&m_timer, &QTimer::timeout, this, &gui_application::UpdatePlaytime);

View file

@ -69,6 +69,8 @@ public:
/** Call this method before calling app.exec */
bool Init() override;
static s32 get_language_id();
std::unique_ptr<gs_frame> get_gs_frame();
main_window* m_main_window = nullptr;
@ -90,6 +92,8 @@ private:
void UpdatePlaytime();
void StopPlaytime();
void set_language_code(QString language_code);
class native_event_filter : public QAbstractNativeEventFilter
{
public:
@ -99,6 +103,7 @@ private:
QTranslator m_translator;
QString m_language_code;
static s32 m_language_id;
QTimer m_timer;
QElapsedTimer m_timer_playtime;

View file

@ -2413,9 +2413,9 @@ void main_window::RepaintGui()
Q_EMIT RequestDialogRepaint();
}
void main_window::RetranslateUI(const QStringList& language_codes, const QString& language)
void main_window::RetranslateUI(const QStringList& language_codes, const QString& language_code)
{
UpdateLanguageActions(language_codes, language);
UpdateLanguageActions(language_codes, language_code);
ui->retranslateUi(this);

View file

@ -111,7 +111,7 @@ public Q_SLOTS:
void OnAddBreakpoint(u32 addr) const;
void RepaintGui();
void RetranslateUI(const QStringList& language_codes, const QString& language);
void RetranslateUI(const QStringList& language_codes, const QString& language_code);
private Q_SLOTS:
void OnPlayOrPause();

View file

@ -2,6 +2,7 @@
#include "custom_table_widget_item.h"
#include "qt_utils.h"
#include "gui_application.h"
#include "gui_settings.h"
#include "persistent_settings.h"
#include "game_list_delegate.h"
@ -405,11 +406,14 @@ void save_manager_dialog::UpdateIcons()
m_list->resizeRowsToContents();
m_list->resizeColumnToContents(SaveColumns::Icon);
const s32 language_index = gui_application::get_language_id();
const std::string localized_icon = fmt::format("ICON0_%02d.PNG", language_index);
for (int i = 0; i < m_list->rowCount(); ++i)
{
if (movie_item* icon_item = static_cast<movie_item*>(m_list->item(i, SaveColumns::Icon)))
{
icon_item->set_icon_load_func([this, cancel = icon_item->icon_loading_aborted(), dpr](int index)
icon_item->set_icon_load_func([this, cancel = icon_item->icon_loading_aborted(), dpr, localized_icon](int index)
{
if (cancel && cancel->load())
{
@ -428,7 +432,8 @@ void save_manager_dialog::UpdateIcons()
const int idx_real = user_item->data(Qt::UserRole).toInt();
const SaveDataEntry& entry = ::at32(m_save_entries, idx_real);
if (!icon.load(QString::fromStdString(m_dir + entry.dirName + "/ICON0.PNG")))
if (!icon.load(QString::fromStdString(m_dir + entry.dirName + "/" + localized_icon)) &&
!icon.load(QString::fromStdString(m_dir + entry.dirName + "/ICON0.PNG")))
{
gui_log.warning("Loading icon for save %s failed", entry.dirName);
icon = QPixmap(320, 176);

View file

@ -4,6 +4,7 @@
#include "game_list_delegate.h"
#include "qt_utils.h"
#include "game_list.h"
#include "gui_application.h"
#include "gui_settings.h"
#include "progress_dialog.h"
#include "persistent_settings.h"
@ -579,15 +580,18 @@ void trophy_manager_dialog::ResizeGameIcons()
ReadjustGameTable();
const s32 language_index = gui_application::get_language_id();
const QString localized_icon = QString::fromStdString(fmt::format("ICON0_%02d.PNG", language_index));
for (int i = 0; i < m_game_table->rowCount(); ++i)
{
if (movie_item* item = static_cast<movie_item*>(m_game_table->item(i, static_cast<int>(gui::trophy_game_list_columns::icon))))
{
const qreal dpr = devicePixelRatioF();
const int trophy_index = item->data(GameUserRole::GameIndex).toInt();
const std::string icon_path = m_trophies_db[trophy_index]->path + "ICON0.PNG";
const QString icon_path = QString::fromStdString(m_trophies_db[trophy_index]->path);
item->set_icon_load_func([this, icon_path, trophy_index, cancel = item->icon_loading_aborted(), dpr](int index)
item->set_icon_load_func([this, icon_path, localized_icon, trophy_index, cancel = item->icon_loading_aborted(), dpr](int index)
{
if (cancel && cancel->load())
{
@ -601,7 +605,8 @@ void trophy_manager_dialog::ResizeGameIcons()
if (!item->data(GameUserRole::GamePixmapLoaded).toBool())
{
// Load game icon
if (!icon.load(QString::fromStdString(icon_path)))
if (!icon.load(icon_path + localized_icon) &&
!icon.load(icon_path + "ICON0.PNG"))
{
gui_log.warning("Could not load trophy game icon from path %s", icon_path);
}