diff --git a/pcsx2-qt/Settings/GameSummaryWidget.cpp b/pcsx2-qt/Settings/GameSummaryWidget.cpp index 3e4239984b..9bbbf8c337 100644 --- a/pcsx2-qt/Settings/GameSummaryWidget.cpp +++ b/pcsx2-qt/Settings/GameSummaryWidget.cpp @@ -63,6 +63,11 @@ GameSummaryWidget::GameSummaryWidget(const GameList::Entry* entry, SettingsDialo connect(m_ui.inputProfile, &QComboBox::currentIndexChanged, this, &GameSummaryWidget::onInputProfileChanged); connect(m_ui.verify, &QAbstractButton::clicked, this, &GameSummaryWidget::onVerifyClicked); connect(m_ui.searchHash, &QAbstractButton::clicked, this, &GameSummaryWidget::onSearchHashClicked); + + bool has_custom_title = false, has_custom_region = false; + GameList::CheckCustomAttributesForPath(m_entry_path, has_custom_title, has_custom_region); + m_ui.restoreTitle->setEnabled(has_custom_title); + m_ui.restoreRegion->setEnabled(has_custom_region); } GameSummaryWidget::~GameSummaryWidget() = default; @@ -88,6 +93,24 @@ void GameSummaryWidget::populateDetails(const GameList::Entry* entry) m_ui.inputProfile->setCurrentIndex(m_ui.inputProfile->findText(QString::fromStdString(profile.value()))); else m_ui.inputProfile->setCurrentIndex(0); + + connect(m_ui.title, &QLineEdit::editingFinished, this, [this]() { + if (m_ui.title->isModified()) + { + setCustomTitle(m_ui.title->text().toStdString()); + m_ui.title->setModified(false); + } + }); + connect(m_ui.restoreTitle, &QAbstractButton::clicked, this, [this]() { + setCustomTitle(""); + }); + + connect(m_ui.region, &QComboBox::currentIndexChanged, this, [this](int index) { + setCustomRegion(index); + }); + connect(m_ui.restoreRegion, &QAbstractButton::clicked, this, [this]() { + setCustomRegion(-1); + }); } void GameSummaryWidget::populateDiscPath(const GameList::Entry* entry) @@ -129,12 +152,7 @@ void GameSummaryWidget::onDiscPathChanged(const QString& value) // force rescan of elf to update the serial g_main_window->rescanFile(m_entry_path); - - // and re-fill our details (mainly the serial) - auto lock = GameList::GetLock(); - const GameList::Entry* entry = GameList::GetEntryForPath(m_entry_path.c_str()); - if (entry) - populateDetails(entry); + repopulateCurrentDetails(); } void GameSummaryWidget::onDiscPathBrowseClicked() @@ -352,4 +370,31 @@ void GameSummaryWidget::setVerifyResult(QString error) m_ui.verifyResult->setPlainText(error); m_ui.verifyResult->setVisible(true); m_ui.searchHash->setVisible(true); -} \ No newline at end of file +} + +void GameSummaryWidget::repopulateCurrentDetails() +{ + auto lock = GameList::GetLock(); + const GameList::Entry* entry = GameList::GetEntryForPath(m_entry_path.c_str()); + if (entry) + { + populateDetails(entry); + m_dialog->setWindowTitle(QString::fromStdString(entry->title)); + } +} + +void GameSummaryWidget::setCustomTitle(const std::string& text) +{ + m_ui.restoreTitle->setEnabled(!text.empty()); + + GameList::SaveCustomTitleForPath(m_entry_path, text); + repopulateCurrentDetails(); +} + +void GameSummaryWidget::setCustomRegion(int region) +{ + m_ui.restoreRegion->setEnabled(region >= 0); + + GameList::SaveCustomRegionForPath(m_entry_path, region); + repopulateCurrentDetails(); +} diff --git a/pcsx2-qt/Settings/GameSummaryWidget.h b/pcsx2-qt/Settings/GameSummaryWidget.h index fba321a902..75f1e78788 100644 --- a/pcsx2-qt/Settings/GameSummaryWidget.h +++ b/pcsx2-qt/Settings/GameSummaryWidget.h @@ -47,6 +47,10 @@ private: void populateDiscPath(const GameList::Entry* entry); void populateTrackList(const GameList::Entry* entry); void setVerifyResult(QString error); + void repopulateCurrentDetails(); + + void setCustomTitle(const std::string& text); + void setCustomRegion(int region); Ui::GameSummaryWidget m_ui; SettingsDialog* m_dialog; diff --git a/pcsx2-qt/Settings/GameSummaryWidget.ui b/pcsx2-qt/Settings/GameSummaryWidget.ui index 307e516a71..9d00f7bd17 100644 --- a/pcsx2-qt/Settings/GameSummaryWidget.ui +++ b/pcsx2-qt/Settings/GameSummaryWidget.ui @@ -34,11 +34,25 @@ - - - true - - + + + + + Clear the line to restore the original title... + + + + + + + false + + + Restore + + + + @@ -140,170 +154,178 @@ - - - false - - - - 0 - 0 - - - - true - - - - NTSC-B (Brazil) - + + + + + + 0 + 0 + + + + + NTSC-B (Brazil) + + + + + NTSC-C (China) + + + + + NTSC-HK (Hong Kong) + + + + + NTSC-J (Japan) + + + + + NTSC-K (Korea) + + + + + NTSC-T (Taiwan) + + + + + NTSC-U (US) + + + + + Other + + + + + PAL-A (Australia) + + + + + PAL-AF (South Africa) + + + + + PAL-AU (Austria) + + + + + PAL-BE (Belgium) + + + + + PAL-E (Europe/Australia) + + + + + PAL-F (France) + + + + + PAL-FI (Finland) + + + + + PAL-G (Germany) + + + + + PAL-GR (Greece) + + + + + PAL-I (Italy) + + + + + PAL-IN (India) + + + + + PAL-M (Europe/Australia) + + + + + PAL-NL (Netherlands) + + + + + PAL-NO (Norway) + + + + + PAL-P (Portugal) + + + + + PAL-PL (Poland) + + + + + PAL-R (Russia) + + + + + PAL-S (Spain) + + + + + PAL-SC (Scandinavia) + + + + + PAL-SW (Sweden) + + + + + PAL-SWI (Switzerland) + + + + + PAL-UK (United Kingdom) + + + + + + + + false + + + Restore + + - - - NTSC-C (China) - - - - - NTSC-HK (Hong Kong) - - - - - NTSC-J (Japan) - - - - - NTSC-K (Korea) - - - - - NTSC-T (Taiwan) - - - - - NTSC-U (US) - - - - - Other - - - - - PAL-A (Australia) - - - - - PAL-AF (South Africa) - - - - - PAL-AU (Austria) - - - - - PAL-BE (Belgium) - - - - - PAL-E (Europe/Australia) - - - - - PAL-F (France) - - - - - PAL-FI (Finland) - - - - - PAL-G (Germany) - - - - - PAL-GR (Greece) - - - - - PAL-I (Italy) - - - - - PAL-IN (India) - - - - - PAL-M (Europe/Australia) - - - - - PAL-NL (Netherlands) - - - - - PAL-NO (Norway) - - - - - PAL-P (Portugal) - - - - - PAL-PL (Poland) - - - - - PAL-R (Russia) - - - - - PAL-S (Spain) - - - - - PAL-SC (Scandinavia) - - - - - PAL-SW (Sweden) - - - - - PAL-SWI (Switzerland) - - - - - PAL-UK (United Kingdom) - - - + @@ -488,22 +510,22 @@ 60 - - true - false + + true + - - Search on Redump.org... - false + + Search on Redump.org... + diff --git a/pcsx2-qt/Settings/SettingsDialog.cpp b/pcsx2-qt/Settings/SettingsDialog.cpp index 7277937fc3..1ef4ae1475 100644 --- a/pcsx2-qt/Settings/SettingsDialog.cpp +++ b/pcsx2-qt/Settings/SettingsDialog.cpp @@ -12,7 +12,7 @@ * You should have received a copy of the GNU General Public License along with PCSX2. * If not, see . */ - + #include "PrecompiledHeader.h" #include "MainWindow.h" @@ -59,11 +59,12 @@ SettingsDialog::SettingsDialog(QWidget* parent) } SettingsDialog::SettingsDialog(QWidget* parent, std::unique_ptr sif, const GameList::Entry* game, - std::string serial, u32 disc_crc) + std::string serial, u32 disc_crc, QString filename) : QDialog(parent) , m_sif(std::move(sif)) , m_serial(std::move(serial)) , m_disc_crc(disc_crc) + , m_filename(std::move(filename)) { setupUi(game); @@ -80,9 +81,9 @@ void SettingsDialog::setupUi(const GameList::Entry* game) if (isPerGameSettings()) { QString summary = tr("Summary
This page shows details about the selected game. Changing the Input " - "Profile will set the controller binding scheme for this game to whichever profile is chosen, instead " - "of the default (Shared) configuration. The track list and dump verification can be used to determine " - "if your disc image matches a known good dump. If it does not match, the game may be broken."); + "Profile will set the controller binding scheme for this game to whichever profile is chosen, instead " + "of the default (Shared) configuration. The track list and dump verification can be used to determine " + "if your disc image matches a known good dump. If it does not match, the game may be broken."); if (game) { addWidget(new GameSummaryWidget(game, this, m_ui.settingsContainer), tr("Summary"), @@ -153,7 +154,7 @@ void SettingsDialog::setupUi(const GameList::Entry* game) addWidget(m_memory_card_settings = new MemoryCardSettingsWidget(this, m_ui.settingsContainer), tr("Memory Cards"), QStringLiteral("memcard-line"), tr("Memory Card Settings
Create and configure Memory Cards here.

Mouse over an option for " - "additional information.")); + "additional information.")); addWidget(m_dev9_settings = new DEV9SettingsWidget(this, m_ui.settingsContainer), tr("Network & HDD"), QStringLiteral("global-line"), tr("Network & HDD Settings
These options control the network connectivity and internal HDD storage of the " @@ -330,6 +331,18 @@ bool SettingsDialog::eventFilter(QObject* object, QEvent* event) return QDialog::eventFilter(object, event); } +void SettingsDialog::setWindowTitle(const QString& title) +{ + if (m_filename.isEmpty()) + { + QDialog::setWindowTitle(title); + } + else + { + QDialog::setWindowTitle(QStringLiteral("%1 [%2]").arg(title, m_filename)); + } +} + bool SettingsDialog::getEffectiveBoolValue(const char* section, const char* key, bool default_value) const { bool value; @@ -544,16 +557,12 @@ void SettingsDialog::openGamePropertiesDialog(const GameList::Entry* game, const } std::string filename(VMManager::GetGameSettingsPath(serial, disc_crc)); - std::unique_ptr sif = std::make_unique(std::move(filename)); + std::unique_ptr sif = std::make_unique(filename); if (FileSystem::FileExists(sif->GetFileName().c_str())) sif->Load(); - const QString window_title(tr("%1 [%2]") - .arg(QtUtils::StringViewToQString(title)) - .arg(QtUtils::StringViewToQString(Path::GetFileName(sif->GetFileName())))); - - SettingsDialog* dialog = new SettingsDialog(g_main_window, std::move(sif), game, std::move(serial), disc_crc); - dialog->setWindowTitle(window_title); + SettingsDialog* dialog = new SettingsDialog(g_main_window, std::move(sif), game, std::move(serial), disc_crc, QtUtils::StringViewToQString(Path::GetFileName(filename))); + dialog->setWindowTitle(QtUtils::StringViewToQString(title)); dialog->setModal(false); dialog->show(); } diff --git a/pcsx2-qt/Settings/SettingsDialog.h b/pcsx2-qt/Settings/SettingsDialog.h index 398e3ef550..71b0907c16 100644 --- a/pcsx2-qt/Settings/SettingsDialog.h +++ b/pcsx2-qt/Settings/SettingsDialog.h @@ -51,7 +51,7 @@ class SettingsDialog final : public QDialog public: explicit SettingsDialog(QWidget* parent); - SettingsDialog(QWidget* parent, std::unique_ptr sif, const GameList::Entry* game, std::string serial, u32 disc_crc); + SettingsDialog(QWidget* parent, std::unique_ptr sif, const GameList::Entry* game, std::string serial, u32 disc_crc, QString filename = QString()); ~SettingsDialog(); static void openGamePropertiesDialog(const GameList::Entry* game, const std::string_view& title, std::string serial, u32 disc_crc); @@ -79,6 +79,8 @@ public: void registerWidgetHelp(QObject* object, QString title, QString recommended_value, QString text); bool eventFilter(QObject* object, QEvent* event) override; + void setWindowTitle(const QString& title); + QString getCategory() const; void setCategory(const char* category); @@ -145,6 +147,8 @@ private: QObject* m_current_help_widget = nullptr; QMap m_widget_help_text_map; + QString m_filename; + std::string m_serial; u32 m_disc_crc; }; diff --git a/pcsx2/GameList.cpp b/pcsx2/GameList.cpp index b601da3369..718a90f1c3 100644 --- a/pcsx2/GameList.cpp +++ b/pcsx2/GameList.cpp @@ -19,6 +19,7 @@ #include "Elfheader.h" #include "GameList.h" #include "Host.h" +#include "INISettingsInterface.h" #include "VMManager.h" #include "common/Assertions.h" @@ -75,10 +76,10 @@ namespace GameList static bool GetGameListEntryFromCache(const std::string& path, GameList::Entry* entry); static void ScanDirectory(const char* path, bool recursive, bool only_cache, const std::vector& excluded_paths, - const PlayedTimeMap& played_time_map, ProgressCallback* progress); + const PlayedTimeMap& played_time_map, const INISettingsInterface& custom_attributes_ini, ProgressCallback* progress); static bool AddFileFromCache(const std::string& path, std::time_t timestamp, const PlayedTimeMap& played_time_map); - static bool ScanFile( - std::string path, std::time_t timestamp, std::unique_lock& lock, const PlayedTimeMap& played_time_map); + static bool ScanFile(std::string path, std::time_t timestamp, std::unique_lock& lock, + const PlayedTimeMap& played_time_map, const INISettingsInterface& custom_attributes_ini); static void LoadCache(); static bool LoadEntriesFromCache(std::FILE* stream); @@ -94,6 +95,8 @@ namespace GameList static PlayedTimeMap LoadPlayedTimeMap(const std::string& path); static PlayedTimeEntry UpdatePlayedTimeFile( const std::string& path, const std::string& serial, std::time_t last_time, std::time_t add_time); + + static std::string GetCustomPropertiesFile(); } // namespace GameList static std::vector s_entries; @@ -586,7 +589,7 @@ static bool IsPathExcluded(const std::vector& excluded_paths, const } void GameList::ScanDirectory(const char* path, bool recursive, bool only_cache, const std::vector& excluded_paths, - const PlayedTimeMap& played_time_map, ProgressCallback* progress) + const PlayedTimeMap& played_time_map, const INISettingsInterface& custom_attributes_ini, ProgressCallback* progress) { Console.WriteLn("Scanning %s%s", path, recursive ? " (recursively)" : ""); @@ -619,7 +622,7 @@ void GameList::ScanDirectory(const char* path, bool recursive, bool only_cache, } progress->SetFormattedStatusText("Scanning '%s'...", FileSystem::GetDisplayNameFromPath(ffd.FileName).c_str()); - ScanFile(std::move(ffd.FileName), ffd.ModificationTime, lock, played_time_map); + ScanFile(std::move(ffd.FileName), ffd.ModificationTime, lock, played_time_map, custom_attributes_ini); progress->SetProgressValue(files_scanned); } @@ -648,8 +651,8 @@ bool GameList::AddFileFromCache(const std::string& path, std::time_t timestamp, return true; } -bool GameList::ScanFile( - std::string path, std::time_t timestamp, std::unique_lock& lock, const PlayedTimeMap& played_time_map) +bool GameList::ScanFile(std::string path, std::time_t timestamp, std::unique_lock& lock, + const PlayedTimeMap& played_time_map, const INISettingsInterface& custom_attributes_ini) { // don't block UI while scanning lock.unlock(); @@ -675,13 +678,28 @@ bool GameList::ScanFile( return true; } - auto iter = played_time_map.find(entry.serial); + const auto iter = played_time_map.find(entry.serial); if (iter != played_time_map.end()) { entry.last_played_time = iter->second.last_played_time; entry.total_played_time = iter->second.total_played_time; } + auto custom_title = custom_attributes_ini.GetOptionalStringValue(entry.path.c_str(), "Title"); + if (custom_title) + { + entry.title = std::move(custom_title.value()); + } + const auto custom_region = custom_attributes_ini.GetOptionalIntValue(entry.path.c_str(), "Region"); + if (custom_region) + { + const int custom_region_value = custom_region.value(); + if (custom_region_value >= 0 && custom_region_value < static_cast(Region::Count)) + { + entry.region = static_cast(custom_region_value); + } + } + lock.lock(); // remove if present @@ -764,6 +782,8 @@ void GameList::Refresh(bool invalidate_cache, bool only_cache, ProgressCallback* const std::vector dirs(Host::GetBaseStringListSetting("GameList", "Paths")); const std::vector recursive_dirs(Host::GetBaseStringListSetting("GameList", "RecursivePaths")); const PlayedTimeMap played_time(LoadPlayedTimeMap(GetPlayedTimeFile())); + INISettingsInterface custom_attributes_ini(GetCustomPropertiesFile()); + custom_attributes_ini.Load(); if (!dirs.empty() || !recursive_dirs.empty()) { @@ -777,7 +797,7 @@ void GameList::Refresh(bool invalidate_cache, bool only_cache, ProgressCallback* if (progress->IsCancelled()) break; - ScanDirectory(dir.c_str(), false, only_cache, excluded_paths, played_time, progress); + ScanDirectory(dir.c_str(), false, only_cache, excluded_paths, played_time, custom_attributes_ini, progress); progress->SetProgressValue(++directory_counter); } for (const std::string& dir : recursive_dirs) @@ -785,7 +805,7 @@ void GameList::Refresh(bool invalidate_cache, bool only_cache, ProgressCallback* if (progress->IsCancelled()) break; - ScanDirectory(dir.c_str(), true, only_cache, excluded_paths, played_time, progress); + ScanDirectory(dir.c_str(), true, only_cache, excluded_paths, played_time, custom_attributes_ini, progress); progress->SetProgressValue(++directory_counter); } } @@ -804,6 +824,8 @@ bool GameList::RescanPath(const std::string& path) std::unique_lock lock(s_mutex); const PlayedTimeMap played_time(LoadPlayedTimeMap(GetPlayedTimeFile())); + INISettingsInterface custom_attributes_ini(GetCustomPropertiesFile()); + custom_attributes_ini.Load(); { // cancel if excluded @@ -813,7 +835,7 @@ bool GameList::RescanPath(const std::string& path) } // re-scan! - if (!ScanFile(path, sd.ModificationTime, lock, played_time)) + if (!ScanFile(path, sd.ModificationTime, lock, played_time, custom_attributes_ini)) return true; // update cache.. this is far from ideal, but since everything's variable length, all we can do. @@ -1319,3 +1341,74 @@ bool GameList::DownloadCovers(const std::vector& url_templates, boo return true; } + +std::string GameList::GetCustomPropertiesFile() +{ + return Path::Combine(EmuFolders::Settings, "custom_properties.ini"); +} + +void GameList::CheckCustomAttributesForPath(const std::string& path, bool& has_custom_title, bool& has_custom_region) +{ + INISettingsInterface names(GetCustomPropertiesFile()); + if (names.Load()) + { + has_custom_title = names.ContainsValue(path.c_str(), "Title"); + has_custom_region = names.ContainsValue(path.c_str(), "Region"); + } +} + +void GameList::SaveCustomTitleForPath(const std::string& path, const std::string& custom_title) +{ + INISettingsInterface names(GetCustomPropertiesFile()); + names.Load(); + + if (!custom_title.empty()) + { + names.SetStringValue(path.c_str(), "Title", custom_title.c_str()); + } + else + { + names.DeleteValue(path.c_str(), "Title"); + } + + if (names.Save()) + { + // Let the cache update by rescanning + RescanPath(path); + } +} + +void GameList::SaveCustomRegionForPath(const std::string& path, int custom_region) +{ + INISettingsInterface names(GetCustomPropertiesFile()); + names.Load(); + + if (custom_region >= 0) + { + names.SetIntValue(path.c_str(), "Region", custom_region); + } + else + { + names.DeleteValue(path.c_str(), "Region"); + } + + if (names.Save()) + { + // Let the cache update by rescanning + RescanPath(path); + } +} + +std::string GameList::GetCustomTitleForPath(const std::string& path) +{ + std::string ret; + + std::unique_lock lock(s_mutex); + const GameList::Entry* entry = GetEntryForPath(path.c_str()); + if (entry) + { + ret = entry->title; + } + + return ret; +} diff --git a/pcsx2/GameList.h b/pcsx2/GameList.h index 5bd4bbc73d..e5442e7070 100644 --- a/pcsx2/GameList.h +++ b/pcsx2/GameList.h @@ -154,4 +154,10 @@ namespace GameList /// the use_serial parameter. save_callback optionall takes the entry and the path the new cover is saved to. bool DownloadCovers(const std::vector& url_templates, bool use_serial = false, ProgressCallback* progress = nullptr, std::function save_callback = {}); + + // Custom properties support + void CheckCustomAttributesForPath(const std::string& path, bool& has_custom_title, bool& has_custom_region); + void SaveCustomTitleForPath(const std::string& path, const std::string& custom_title); + void SaveCustomRegionForPath(const std::string& path, int custom_region); + std::string GetCustomTitleForPath(const std::string& path); } // namespace GameList diff --git a/pcsx2/VMManager.cpp b/pcsx2/VMManager.cpp index cd51ea82f5..f0745b4361 100644 --- a/pcsx2/VMManager.cpp +++ b/pcsx2/VMManager.cpp @@ -849,19 +849,22 @@ void VMManager::UpdateDiscDetails(bool booting) SaveSessionTime(old_serial); + std::string custom_title = GameList::GetCustomTitleForPath(CDVDsys_GetFile(CDVDsys_GetSourceType())); if (serial_is_valid) { if (const GameDatabaseSchema::GameEntry* game = GameDatabase::findGame(s_disc_serial)) { + std::string game_title = custom_title.empty() ? game->name : std::move(custom_title); + // Append the ELF override if we're using it with a disc. if (!s_elf_override.empty()) { title = fmt::format( - "{} [{}]", game->name, Path::GetFileTitle(FileSystem::GetDisplayNameFromPath(s_elf_override))); + "{} [{}]", game_title, Path::GetFileTitle(FileSystem::GetDisplayNameFromPath(s_elf_override))); } else { - title = game->name; + title = std::move(game_title); } memcardFilters = game->memcardFiltersAsString(); @@ -872,6 +875,11 @@ void VMManager::UpdateDiscDetails(bool booting) } } + if (title.empty()) + { + title = std::move(custom_title); + } + if (title.empty()) { if (!s_disc_serial.empty())