GameList: Support adding custom title and region to files

This commit is contained in:
Silent 2023-07-23 20:38:17 +02:00 committed by refractionpcsx2
parent be3ed181c1
commit 0256c4521d
8 changed files with 399 additions and 208 deletions

View File

@ -63,6 +63,11 @@ GameSummaryWidget::GameSummaryWidget(const GameList::Entry* entry, SettingsDialo
connect(m_ui.inputProfile, &QComboBox::currentIndexChanged, this, &GameSummaryWidget::onInputProfileChanged); connect(m_ui.inputProfile, &QComboBox::currentIndexChanged, this, &GameSummaryWidget::onInputProfileChanged);
connect(m_ui.verify, &QAbstractButton::clicked, this, &GameSummaryWidget::onVerifyClicked); connect(m_ui.verify, &QAbstractButton::clicked, this, &GameSummaryWidget::onVerifyClicked);
connect(m_ui.searchHash, &QAbstractButton::clicked, this, &GameSummaryWidget::onSearchHashClicked); 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; 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()))); m_ui.inputProfile->setCurrentIndex(m_ui.inputProfile->findText(QString::fromStdString(profile.value())));
else else
m_ui.inputProfile->setCurrentIndex(0); 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) 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 // force rescan of elf to update the serial
g_main_window->rescanFile(m_entry_path); g_main_window->rescanFile(m_entry_path);
repopulateCurrentDetails();
// 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);
} }
void GameSummaryWidget::onDiscPathBrowseClicked() void GameSummaryWidget::onDiscPathBrowseClicked()
@ -352,4 +370,31 @@ void GameSummaryWidget::setVerifyResult(QString error)
m_ui.verifyResult->setPlainText(error); m_ui.verifyResult->setPlainText(error);
m_ui.verifyResult->setVisible(true); m_ui.verifyResult->setVisible(true);
m_ui.searchHash->setVisible(true); m_ui.searchHash->setVisible(true);
} }
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();
}

View File

@ -47,6 +47,10 @@ private:
void populateDiscPath(const GameList::Entry* entry); void populateDiscPath(const GameList::Entry* entry);
void populateTrackList(const GameList::Entry* entry); void populateTrackList(const GameList::Entry* entry);
void setVerifyResult(QString error); void setVerifyResult(QString error);
void repopulateCurrentDetails();
void setCustomTitle(const std::string& text);
void setCustomRegion(int region);
Ui::GameSummaryWidget m_ui; Ui::GameSummaryWidget m_ui;
SettingsDialog* m_dialog; SettingsDialog* m_dialog;

View File

@ -34,11 +34,25 @@
</widget> </widget>
</item> </item>
<item row="0" column="1"> <item row="0" column="1">
<widget class="QLineEdit" name="title"> <layout class="QHBoxLayout" name="horizontalLayout_6">
<property name="readOnly"> <item>
<bool>true</bool> <widget class="QLineEdit" name="title">
</property> <property name="placeholderText">
</widget> <string>Clear the line to restore the original title...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="restoreTitle">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Restore</string>
</property>
</widget>
</item>
</layout>
</item> </item>
<item row="1" column="0"> <item row="1" column="0">
<widget class="QLabel" name="label_2"> <widget class="QLabel" name="label_2">
@ -140,170 +154,178 @@
</widget> </widget>
</item> </item>
<item row="5" column="1"> <item row="5" column="1">
<widget class="QComboBox" name="region"> <layout class="QHBoxLayout" name="horizontalLayout_6">
<property name="enabled"> <item>
<bool>false</bool> <widget class="QComboBox" name="region">
</property> <property name="sizePolicy">
<property name="sizePolicy"> <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed"> <horstretch>0</horstretch>
<horstretch>0</horstretch> <verstretch>0</verstretch>
<verstretch>0</verstretch> </sizepolicy>
</sizepolicy> </property>
</property> <item>
<property name="editable"> <property name="text">
<bool>true</bool> <string extracomment="Leave the code as-is, translate the country's name.">NTSC-B (Brazil)</string>
</property> </property>
<item> </item>
<property name="text"> <item>
<string extracomment="Leave the code as-is, translate the country's name.">NTSC-B (Brazil)</string> <property name="text">
</property> <string extracomment="Leave the code as-is, translate the country's name.">NTSC-C (China)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">NTSC-HK (Hong Kong)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">NTSC-J (Japan)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">NTSC-K (Korea)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">NTSC-T (Taiwan)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">NTSC-U (US)</string>
</property>
</item>
<item>
<property name="text">
<string>Other</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-A (Australia)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-AF (South Africa)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-AU (Austria)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-BE (Belgium)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-E (Europe/Australia)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-F (France)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-FI (Finland)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-G (Germany)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-GR (Greece)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-I (Italy)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-IN (India)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-M (Europe/Australia)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-NL (Netherlands)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-NO (Norway)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-P (Portugal)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-PL (Poland)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-R (Russia)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-S (Spain)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-SC (Scandinavia)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-SW (Sweden)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-SWI (Switzerland)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-UK (United Kingdom)</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QPushButton" name="restoreRegion">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Restore</string>
</property>
</widget>
</item> </item>
<item> </layout>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">NTSC-C (China)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">NTSC-HK (Hong Kong)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">NTSC-J (Japan)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">NTSC-K (Korea)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">NTSC-T (Taiwan)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">NTSC-U (US)</string>
</property>
</item>
<item>
<property name="text">
<string>Other</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-A (Australia)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-AF (South Africa)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-AU (Austria)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-BE (Belgium)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-E (Europe/Australia)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-F (France)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-FI (Finland)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-G (Germany)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-GR (Greece)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-I (Italy)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-IN (India)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-M (Europe/Australia)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-NL (Netherlands)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-NO (Norway)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-P (Portugal)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-PL (Poland)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-R (Russia)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-S (Spain)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-SC (Scandinavia)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-SW (Sweden)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-SWI (Switzerland)</string>
</property>
</item>
<item>
<property name="text">
<string extracomment="Leave the code as-is, translate the country's name.">PAL-UK (United Kingdom)</string>
</property>
</item>
</widget>
</item> </item>
<item row="6" column="0"> <item row="6" column="0">
<widget class="QLabel" name="label_7"> <widget class="QLabel" name="label_7">
@ -488,22 +510,22 @@
<height>60</height> <height>60</height>
</size> </size>
</property> </property>
<property name="readOnly">
<bool>true</bool>
</property>
<property name="visible"> <property name="visible">
<bool>false</bool> <bool>false</bool>
</property> </property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget> </widget>
</item> </item>
<item row="1" column="2"> <item row="1" column="2">
<widget class="QPushButton" name="searchHash"> <widget class="QPushButton" name="searchHash">
<property name="text">
<string>Search on Redump.org...</string>
</property>
<property name="visible"> <property name="visible">
<bool>false</bool> <bool>false</bool>
</property> </property>
<property name="text">
<string>Search on Redump.org...</string>
</property>
</widget> </widget>
</item> </item>
</layout> </layout>

View File

@ -12,7 +12,7 @@
* You should have received a copy of the GNU General Public License along with PCSX2. * You should have received a copy of the GNU General Public License along with PCSX2.
* If not, see <http://www.gnu.org/licenses/>. * If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "PrecompiledHeader.h" #include "PrecompiledHeader.h"
#include "MainWindow.h" #include "MainWindow.h"
@ -59,11 +59,12 @@ SettingsDialog::SettingsDialog(QWidget* parent)
} }
SettingsDialog::SettingsDialog(QWidget* parent, std::unique_ptr<SettingsInterface> sif, const GameList::Entry* game, SettingsDialog::SettingsDialog(QWidget* parent, std::unique_ptr<SettingsInterface> sif, const GameList::Entry* game,
std::string serial, u32 disc_crc) std::string serial, u32 disc_crc, QString filename)
: QDialog(parent) : QDialog(parent)
, m_sif(std::move(sif)) , m_sif(std::move(sif))
, m_serial(std::move(serial)) , m_serial(std::move(serial))
, m_disc_crc(disc_crc) , m_disc_crc(disc_crc)
, m_filename(std::move(filename))
{ {
setupUi(game); setupUi(game);
@ -80,9 +81,9 @@ void SettingsDialog::setupUi(const GameList::Entry* game)
if (isPerGameSettings()) if (isPerGameSettings())
{ {
QString summary = tr("<strong>Summary</strong><hr>This page shows details about the selected game. Changing the Input " QString summary = tr("<strong>Summary</strong><hr>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 " "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 " "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 your disc image matches a known good dump. If it does not match, the game may be broken.");
if (game) if (game)
{ {
addWidget(new GameSummaryWidget(game, this, m_ui.settingsContainer), tr("Summary"), 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"), addWidget(m_memory_card_settings = new MemoryCardSettingsWidget(this, m_ui.settingsContainer), tr("Memory Cards"),
QStringLiteral("memcard-line"), QStringLiteral("memcard-line"),
tr("<strong>Memory Card Settings</strong><hr>Create and configure Memory Cards here.<br><br>Mouse over an option for " tr("<strong>Memory Card Settings</strong><hr>Create and configure Memory Cards here.<br><br>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"), addWidget(m_dev9_settings = new DEV9SettingsWidget(this, m_ui.settingsContainer), tr("Network & HDD"), QStringLiteral("global-line"),
tr("<strong>Network & HDD Settings</strong><hr>These options control the network connectivity and internal HDD storage of the " tr("<strong>Network & HDD Settings</strong><hr>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); 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 SettingsDialog::getEffectiveBoolValue(const char* section, const char* key, bool default_value) const
{ {
bool value; bool value;
@ -544,16 +557,12 @@ void SettingsDialog::openGamePropertiesDialog(const GameList::Entry* game, const
} }
std::string filename(VMManager::GetGameSettingsPath(serial, disc_crc)); std::string filename(VMManager::GetGameSettingsPath(serial, disc_crc));
std::unique_ptr<INISettingsInterface> sif = std::make_unique<INISettingsInterface>(std::move(filename)); std::unique_ptr<INISettingsInterface> sif = std::make_unique<INISettingsInterface>(filename);
if (FileSystem::FileExists(sif->GetFileName().c_str())) if (FileSystem::FileExists(sif->GetFileName().c_str()))
sif->Load(); sif->Load();
const QString window_title(tr("%1 [%2]") SettingsDialog* dialog = new SettingsDialog(g_main_window, std::move(sif), game, std::move(serial), disc_crc, QtUtils::StringViewToQString(Path::GetFileName(filename)));
.arg(QtUtils::StringViewToQString(title)) dialog->setWindowTitle(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);
dialog->setModal(false); dialog->setModal(false);
dialog->show(); dialog->show();
} }

View File

@ -51,7 +51,7 @@ class SettingsDialog final : public QDialog
public: public:
explicit SettingsDialog(QWidget* parent); explicit SettingsDialog(QWidget* parent);
SettingsDialog(QWidget* parent, std::unique_ptr<SettingsInterface> sif, const GameList::Entry* game, std::string serial, u32 disc_crc); SettingsDialog(QWidget* parent, std::unique_ptr<SettingsInterface> sif, const GameList::Entry* game, std::string serial, u32 disc_crc, QString filename = QString());
~SettingsDialog(); ~SettingsDialog();
static void openGamePropertiesDialog(const GameList::Entry* game, const std::string_view& title, std::string serial, u32 disc_crc); 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); void registerWidgetHelp(QObject* object, QString title, QString recommended_value, QString text);
bool eventFilter(QObject* object, QEvent* event) override; bool eventFilter(QObject* object, QEvent* event) override;
void setWindowTitle(const QString& title);
QString getCategory() const; QString getCategory() const;
void setCategory(const char* category); void setCategory(const char* category);
@ -145,6 +147,8 @@ private:
QObject* m_current_help_widget = nullptr; QObject* m_current_help_widget = nullptr;
QMap<QObject*, QString> m_widget_help_text_map; QMap<QObject*, QString> m_widget_help_text_map;
QString m_filename;
std::string m_serial; std::string m_serial;
u32 m_disc_crc; u32 m_disc_crc;
}; };

View File

@ -19,6 +19,7 @@
#include "Elfheader.h" #include "Elfheader.h"
#include "GameList.h" #include "GameList.h"
#include "Host.h" #include "Host.h"
#include "INISettingsInterface.h"
#include "VMManager.h" #include "VMManager.h"
#include "common/Assertions.h" #include "common/Assertions.h"
@ -75,10 +76,10 @@ namespace GameList
static bool GetGameListEntryFromCache(const std::string& path, GameList::Entry* entry); static bool GetGameListEntryFromCache(const std::string& path, GameList::Entry* entry);
static void ScanDirectory(const char* path, bool recursive, bool only_cache, const std::vector<std::string>& excluded_paths, static void ScanDirectory(const char* path, bool recursive, bool only_cache, const std::vector<std::string>& 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 AddFileFromCache(const std::string& path, std::time_t timestamp, const PlayedTimeMap& played_time_map);
static bool ScanFile( static bool ScanFile(std::string path, std::time_t timestamp, std::unique_lock<std::recursive_mutex>& lock,
std::string path, std::time_t timestamp, std::unique_lock<std::recursive_mutex>& lock, const PlayedTimeMap& played_time_map); const PlayedTimeMap& played_time_map, const INISettingsInterface& custom_attributes_ini);
static void LoadCache(); static void LoadCache();
static bool LoadEntriesFromCache(std::FILE* stream); static bool LoadEntriesFromCache(std::FILE* stream);
@ -94,6 +95,8 @@ namespace GameList
static PlayedTimeMap LoadPlayedTimeMap(const std::string& path); static PlayedTimeMap LoadPlayedTimeMap(const std::string& path);
static PlayedTimeEntry UpdatePlayedTimeFile( static PlayedTimeEntry UpdatePlayedTimeFile(
const std::string& path, const std::string& serial, std::time_t last_time, std::time_t add_time); const std::string& path, const std::string& serial, std::time_t last_time, std::time_t add_time);
static std::string GetCustomPropertiesFile();
} // namespace GameList } // namespace GameList
static std::vector<GameList::Entry> s_entries; static std::vector<GameList::Entry> s_entries;
@ -586,7 +589,7 @@ static bool IsPathExcluded(const std::vector<std::string>& excluded_paths, const
} }
void GameList::ScanDirectory(const char* path, bool recursive, bool only_cache, const std::vector<std::string>& excluded_paths, void GameList::ScanDirectory(const char* path, bool recursive, bool only_cache, const std::vector<std::string>& 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)" : ""); 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()); 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); progress->SetProgressValue(files_scanned);
} }
@ -648,8 +651,8 @@ bool GameList::AddFileFromCache(const std::string& path, std::time_t timestamp,
return true; return true;
} }
bool GameList::ScanFile( bool GameList::ScanFile(std::string path, std::time_t timestamp, std::unique_lock<std::recursive_mutex>& lock,
std::string path, std::time_t timestamp, std::unique_lock<std::recursive_mutex>& lock, const PlayedTimeMap& played_time_map) const PlayedTimeMap& played_time_map, const INISettingsInterface& custom_attributes_ini)
{ {
// don't block UI while scanning // don't block UI while scanning
lock.unlock(); lock.unlock();
@ -675,13 +678,28 @@ bool GameList::ScanFile(
return true; 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()) if (iter != played_time_map.end())
{ {
entry.last_played_time = iter->second.last_played_time; entry.last_played_time = iter->second.last_played_time;
entry.total_played_time = iter->second.total_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<int>(Region::Count))
{
entry.region = static_cast<Region>(custom_region_value);
}
}
lock.lock(); lock.lock();
// remove if present // remove if present
@ -764,6 +782,8 @@ void GameList::Refresh(bool invalidate_cache, bool only_cache, ProgressCallback*
const std::vector<std::string> dirs(Host::GetBaseStringListSetting("GameList", "Paths")); const std::vector<std::string> dirs(Host::GetBaseStringListSetting("GameList", "Paths"));
const std::vector<std::string> recursive_dirs(Host::GetBaseStringListSetting("GameList", "RecursivePaths")); const std::vector<std::string> recursive_dirs(Host::GetBaseStringListSetting("GameList", "RecursivePaths"));
const PlayedTimeMap played_time(LoadPlayedTimeMap(GetPlayedTimeFile())); const PlayedTimeMap played_time(LoadPlayedTimeMap(GetPlayedTimeFile()));
INISettingsInterface custom_attributes_ini(GetCustomPropertiesFile());
custom_attributes_ini.Load();
if (!dirs.empty() || !recursive_dirs.empty()) if (!dirs.empty() || !recursive_dirs.empty())
{ {
@ -777,7 +797,7 @@ void GameList::Refresh(bool invalidate_cache, bool only_cache, ProgressCallback*
if (progress->IsCancelled()) if (progress->IsCancelled())
break; 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); progress->SetProgressValue(++directory_counter);
} }
for (const std::string& dir : recursive_dirs) for (const std::string& dir : recursive_dirs)
@ -785,7 +805,7 @@ void GameList::Refresh(bool invalidate_cache, bool only_cache, ProgressCallback*
if (progress->IsCancelled()) if (progress->IsCancelled())
break; 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); progress->SetProgressValue(++directory_counter);
} }
} }
@ -804,6 +824,8 @@ bool GameList::RescanPath(const std::string& path)
std::unique_lock lock(s_mutex); std::unique_lock lock(s_mutex);
const PlayedTimeMap played_time(LoadPlayedTimeMap(GetPlayedTimeFile())); const PlayedTimeMap played_time(LoadPlayedTimeMap(GetPlayedTimeFile()));
INISettingsInterface custom_attributes_ini(GetCustomPropertiesFile());
custom_attributes_ini.Load();
{ {
// cancel if excluded // cancel if excluded
@ -813,7 +835,7 @@ bool GameList::RescanPath(const std::string& path)
} }
// re-scan! // re-scan!
if (!ScanFile(path, sd.ModificationTime, lock, played_time)) if (!ScanFile(path, sd.ModificationTime, lock, played_time, custom_attributes_ini))
return true; return true;
// update cache.. this is far from ideal, but since everything's variable length, all we can do. // 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<std::string>& url_templates, boo
return true; 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;
}

View File

@ -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. /// the use_serial parameter. save_callback optionall takes the entry and the path the new cover is saved to.
bool DownloadCovers(const std::vector<std::string>& url_templates, bool use_serial = false, ProgressCallback* progress = nullptr, bool DownloadCovers(const std::vector<std::string>& url_templates, bool use_serial = false, ProgressCallback* progress = nullptr,
std::function<void(const Entry*, std::string)> save_callback = {}); std::function<void(const Entry*, std::string)> 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 } // namespace GameList

View File

@ -849,19 +849,22 @@ void VMManager::UpdateDiscDetails(bool booting)
SaveSessionTime(old_serial); SaveSessionTime(old_serial);
std::string custom_title = GameList::GetCustomTitleForPath(CDVDsys_GetFile(CDVDsys_GetSourceType()));
if (serial_is_valid) if (serial_is_valid)
{ {
if (const GameDatabaseSchema::GameEntry* game = GameDatabase::findGame(s_disc_serial)) 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. // Append the ELF override if we're using it with a disc.
if (!s_elf_override.empty()) if (!s_elf_override.empty())
{ {
title = fmt::format( title = fmt::format(
"{} [{}]", game->name, Path::GetFileTitle(FileSystem::GetDisplayNameFromPath(s_elf_override))); "{} [{}]", game_title, Path::GetFileTitle(FileSystem::GetDisplayNameFromPath(s_elf_override)));
} }
else else
{ {
title = game->name; title = std::move(game_title);
} }
memcardFilters = game->memcardFiltersAsString(); memcardFilters = game->memcardFiltersAsString();
@ -872,6 +875,11 @@ void VMManager::UpdateDiscDetails(bool booting)
} }
} }
if (title.empty())
{
title = std::move(custom_title);
}
if (title.empty()) if (title.empty())
{ {
if (!s_disc_serial.empty()) if (!s_disc_serial.empty())