From ff14e8aede6f0e852d346ac27509fb2897b606f4 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sat, 17 Apr 2021 14:23:47 +1000 Subject: [PATCH] Add JSON game database to replace dat parsing --- scripts/merge_gamedb.py | 20 ++ src/core/system.cpp | 5 + src/core/system.h | 1 + src/duckstation-qt/gamelistmodel.cpp | 55 +++- src/duckstation-qt/gamelistmodel.h | 5 + src/duckstation-qt/gamelistsettingswidget.cpp | 162 ------------ src/duckstation-qt/gamelistsettingswidget.h | 3 - src/duckstation-qt/gamelistsettingswidget.ui | 17 -- src/duckstation-qt/gamelistwidget.cpp | 31 ++- src/duckstation-qt/gamepropertiesdialog.cpp | 21 +- src/duckstation-qt/qtutils.cpp | 19 +- src/duckstation-qt/settingsdialog.cpp | 6 +- src/frontend-common/CMakeLists.txt | 2 + src/frontend-common/common_host_interface.cpp | 66 ++++- src/frontend-common/common_host_interface.h | 4 + src/frontend-common/frontend-common.vcxproj | 2 + .../frontend-common.vcxproj.filters | 2 + src/frontend-common/fullscreen_ui.cpp | 19 +- src/frontend-common/game_database.cpp | 225 ++++++++++++++++ src/frontend-common/game_database.h | 45 ++++ src/frontend-common/game_list.cpp | 242 ++++++------------ src/frontend-common/game_list.h | 34 ++- src/frontend-common/game_settings.cpp | 4 - src/frontend-common/game_settings.h | 1 - 24 files changed, 594 insertions(+), 397 deletions(-) create mode 100644 scripts/merge_gamedb.py create mode 100644 src/frontend-common/game_database.cpp create mode 100644 src/frontend-common/game_database.h diff --git a/scripts/merge_gamedb.py b/scripts/merge_gamedb.py new file mode 100644 index 000000000..b4f5880cb --- /dev/null +++ b/scripts/merge_gamedb.py @@ -0,0 +1,20 @@ +import json +import sys +import os + +if __name__ == "__main__": + if (len(sys.argv) < 3): + print("usage: %s " % sys.argv[0]) + sys.exit(1) + + games = [] + for file in os.listdir(sys.argv[1]): + with open(os.path.join(sys.argv[1], file), "r") as f: + fgames = json.load(f) + games.extend(list(fgames)) + + + with open(sys.argv[2], "w") as f: + json.dump(games, f, indent=1) + + print("Wrote %s" % sys.argv[2]) \ No newline at end of file diff --git a/src/core/system.cpp b/src/core/system.cpp index 3693c39d6..5b61521a2 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -414,6 +414,11 @@ std::string GetGameCodeForImage(CDImage* cdi, bool fallback_to_hash) if (!fallback_to_hash) return {}; + return GetGameHashCodeForImage(cdi); +} + +std::string GetGameHashCodeForImage(CDImage* cdi) +{ std::string exe_name; std::vector exe_buffer; if (!ReadExecutableFromImage(cdi, &exe_name, &exe_buffer)) diff --git a/src/core/system.h b/src/core/system.h index adda418e0..4e15d8bf8 100644 --- a/src/core/system.h +++ b/src/core/system.h @@ -72,6 +72,7 @@ ConsoleRegion GetConsoleRegionForDiscRegion(DiscRegion region); std::string GetExecutableNameForImage(CDImage* cdi); bool ReadExecutableFromImage(CDImage* cdi, std::string* out_executable_name, std::vector* out_executable_data); +std::string GetGameHashCodeForImage(CDImage* cdi); std::string GetGameCodeForImage(CDImage* cdi, bool fallback_to_hash); std::string GetGameCodeForPath(const char* image_path, bool fallback_to_hash); DiscRegion GetRegionForCode(std::string_view code); diff --git a/src/duckstation-qt/gamelistmodel.cpp b/src/duckstation-qt/gamelistmodel.cpp index f2f481c33..20e73e956 100644 --- a/src/duckstation-qt/gamelistmodel.cpp +++ b/src/duckstation-qt/gamelistmodel.cpp @@ -2,11 +2,14 @@ #include "common/file_system.h" #include "common/string_util.h" #include "core/system.h" +#include +#include #include #include static constexpr std::array s_column_names = { - {"Type", "Code", "Title", "File Title", "Size", "Region", "Compatibility", "Cover"}}; + {"Type", "Code", "Title", "File Title", "Developer", "Publisher", "Genre", "Year", "Players", "Size", "Region", + "Compatibility", "Cover"}}; static constexpr int COVER_ART_WIDTH = 512; static constexpr int COVER_ART_HEIGHT = 512; @@ -164,6 +167,36 @@ QVariant GameListModel::data(const QModelIndex& index, int role) const return QString::fromUtf8(file_title.data(), static_cast(file_title.length())); } + case Column_Developer: + return QString::fromStdString(ge.developer); + + case Column_Publisher: + return QString::fromStdString(ge.publisher); + + case Column_Genre: + return QString::fromStdString(ge.genre); + + case Column_Year: + { + if (ge.release_date != 0) + { + return QStringLiteral("%1").arg( + QDateTime::fromSecsSinceEpoch(static_cast(ge.release_date), Qt::UTC).date().year()); + } + else + { + return QString(); + } + } + + case Column_Players: + { + if (ge.min_players == ge.max_players) + return QStringLiteral("%1").arg(ge.min_players); + else + return QStringLiteral("%1-%2").arg(ge.min_players).arg(ge.max_players); + } + case Column_Size: return QString("%1 MB").arg(static_cast(ge.total_size) / 1048576.0, 0, 'f', 2); @@ -200,6 +233,21 @@ QVariant GameListModel::data(const QModelIndex& index, int role) const return QString::fromUtf8(file_title.data(), static_cast(file_title.length())); } + case Column_Developer: + return QString::fromStdString(ge.developer); + + case Column_Publisher: + return QString::fromStdString(ge.publisher); + + case Column_Genre: + return QString::fromStdString(ge.genre); + + case Column_Year: + return QDateTime::fromSecsSinceEpoch(static_cast(ge.release_date), Qt::UTC).date().year(); + + case Column_Players: + return static_cast(ge.max_players); + case Column_Region: return static_cast(ge.region); @@ -422,6 +470,11 @@ void GameListModel::setColumnDisplayNames() m_column_display_names[Column_Code] = tr("Code"); m_column_display_names[Column_Title] = tr("Title"); m_column_display_names[Column_FileTitle] = tr("File Title"); + m_column_display_names[Column_Developer] = tr("Developer"); + m_column_display_names[Column_Publisher] = tr("Publisher"); + m_column_display_names[Column_Genre] = tr("Genre"); + m_column_display_names[Column_Year] = tr("Year"); + m_column_display_names[Column_Players] = tr("Players"); m_column_display_names[Column_Size] = tr("Size"); m_column_display_names[Column_Region] = tr("Region"); m_column_display_names[Column_Compatibility] = tr("Compatibility"); diff --git a/src/duckstation-qt/gamelistmodel.h b/src/duckstation-qt/gamelistmodel.h index 8331be250..beeec8729 100644 --- a/src/duckstation-qt/gamelistmodel.h +++ b/src/duckstation-qt/gamelistmodel.h @@ -19,6 +19,11 @@ public: Column_Code, Column_Title, Column_FileTitle, + Column_Developer, + Column_Publisher, + Column_Genre, + Column_Year, + Column_Players, Column_Size, Column_Region, Column_Compatibility, diff --git a/src/duckstation-qt/gamelistsettingswidget.cpp b/src/duckstation-qt/gamelistsettingswidget.cpp index 5c775ebb6..a44c1e1db 100644 --- a/src/duckstation-qt/gamelistsettingswidget.cpp +++ b/src/duckstation-qt/gamelistsettingswidget.cpp @@ -1,7 +1,6 @@ #include "gamelistsettingswidget.h" #include "common/assert.h" #include "common/file_system.h" -#include "common/minizip_helpers.h" #include "common/string_util.h" #include "frontend-common/game_list.h" #include "gamelistsearchdirectoriesmodel.h" @@ -11,18 +10,12 @@ #include #include #include -#include -#include -#include #include #include #include #include -#include #include -static constexpr char REDUMP_DOWNLOAD_URL[] = "http://redump.org/datfile/psx/serial,version,description"; - GameListSettingsWidget::GameListSettingsWidget(QtHostInterface* host_interface, QWidget* parent /* = nullptr */) : QWidget(parent), m_host_interface(host_interface) { @@ -48,8 +41,6 @@ GameListSettingsWidget::GameListSettingsWidget(QtHostInterface* host_interface, &GameListSettingsWidget::onRemoveSearchDirectoryButtonClicked); connect(m_ui.rescanAllGames, &QPushButton::clicked, this, &GameListSettingsWidget::onRescanAllGamesClicked); connect(m_ui.scanForNewGames, &QPushButton::clicked, this, &GameListSettingsWidget::onScanForNewGamesClicked); - connect(m_ui.updateRedumpDatabase, &QPushButton::clicked, this, - &GameListSettingsWidget::onUpdateRedumpDatabaseButtonClicked); } GameListSettingsWidget::~GameListSettingsWidget() = default; @@ -135,156 +126,3 @@ void GameListSettingsWidget::onScanForNewGamesClicked() { m_host_interface->refreshGameList(false, false); } - -void GameListSettingsWidget::onUpdateRedumpDatabaseButtonClicked() -{ - if (QMessageBox::question(this, tr("Download database from redump.org?"), - tr("Do you wish to download the disc database from redump.org?\n\nThis will download " - "approximately 4 megabytes over your current internet connection.")) != QMessageBox::Yes) - { - return; - } - - if (downloadRedumpDatabase(m_host_interface->getUserDirectoryRelativePath("redump.dat"))) - m_host_interface->refreshGameList(true, true); -} - -static bool ExtractRedumpDatabase(const QByteArray& data, const QString& destination_path) -{ - if (data.isEmpty()) - return false; - - unzFile zf = MinizipHelpers::OpenUnzMemoryFile(data.constData(), data.size()); - if (!zf) - { - qCritical() << "unzOpen2_64() failed"; - return false; - } - - // find the first file with a .dat extension (in case there's others) - if (unzGoToFirstFile(zf) != UNZ_OK) - { - qCritical() << "unzGoToFirstFile() failed"; - unzClose(zf); - return false; - } - - int dat_size = 0; - for (;;) - { - char zip_filename_buffer[256]; - unz_file_info64 file_info; - if (unzGetCurrentFileInfo64(zf, &file_info, zip_filename_buffer, sizeof(zip_filename_buffer), nullptr, 0, nullptr, - 0) != UNZ_OK) - { - qCritical() << "unzGetCurrentFileInfo() failed"; - unzClose(zf); - return false; - } - - const char* extension = std::strrchr(zip_filename_buffer, '.'); - if (extension && StringUtil::Strcasecmp(extension, ".dat") == 0 && file_info.uncompressed_size > 0) - { - dat_size = static_cast(file_info.uncompressed_size); - qInfo() << "Found redump dat file in zip: " << zip_filename_buffer << "(" << dat_size << " bytes)"; - break; - } - - if (unzGoToNextFile(zf) != UNZ_OK) - { - qCritical() << "dat file not found in downloaded redump zip"; - unzClose(zf); - return false; - } - } - - if (unzOpenCurrentFile(zf) != UNZ_OK) - { - qCritical() << "unzOpenCurrentFile() failed"; - unzClose(zf); - return false; - } - - QByteArray dat_buffer; - dat_buffer.resize(dat_size); - if (unzReadCurrentFile(zf, dat_buffer.data(), dat_size) != dat_size) - { - qCritical() << "unzReadCurrentFile() failed"; - unzClose(zf); - return false; - } - - unzCloseCurrentFile(zf); - unzClose(zf); - - QFile dat_output_file(destination_path); - if (!dat_output_file.open(QIODevice::WriteOnly | QIODevice::Truncate)) - { - qCritical() << "QFile::open() failed"; - return false; - } - - if (static_cast(dat_output_file.write(dat_buffer)) != dat_buffer.size()) - { - qCritical() << "QFile::write() failed"; - return false; - } - - dat_output_file.close(); - qInfo() << "Wrote redump dat to " << destination_path; - return true; -} - -bool GameListSettingsWidget::downloadRedumpDatabase(const QString& download_path) -{ - Assert(!download_path.isEmpty()); - - QNetworkAccessManager manager; - - QUrl url(QUrl::fromEncoded(QByteArray(REDUMP_DOWNLOAD_URL, sizeof(REDUMP_DOWNLOAD_URL) - 1))); - QNetworkRequest request(url); - - QNetworkReply* reply = manager.get(request); - - QProgressDialog progress(tr("Downloading %1...").arg(REDUMP_DOWNLOAD_URL), tr("Cancel"), 0, 1); - progress.setAutoClose(false); - - connect(reply, &QNetworkReply::downloadProgress, [&progress](quint64 received, quint64 total) { - progress.setRange(0, static_cast(total)); - progress.setValue(static_cast(received)); - }); - - connect(&manager, &QNetworkAccessManager::finished, [this, &progress, &download_path](QNetworkReply* reply) { - if (reply->error() != QNetworkReply::NoError) - { - QMessageBox::critical(this, tr("Download failed"), reply->errorString()); - progress.done(-1); - return; - } - - progress.setRange(0, 100); - progress.setValue(100); - progress.setLabelText(tr("Extracting...")); - QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); - - const QByteArray data = reply->readAll(); - if (!ExtractRedumpDatabase(data, download_path)) - { - QMessageBox::critical(this, tr("Extract failed"), tr("Extracting game database failed.")); - progress.done(-1); - return; - } - - progress.done(1); - }); - - const int result = progress.exec(); - if (result == 0) - { - // cancelled - reply->abort(); - } - - reply->deleteLater(); - return (result == 1); -} diff --git a/src/duckstation-qt/gamelistsettingswidget.h b/src/duckstation-qt/gamelistsettingswidget.h index 8918e3581..11bb9e699 100644 --- a/src/duckstation-qt/gamelistsettingswidget.h +++ b/src/duckstation-qt/gamelistsettingswidget.h @@ -26,14 +26,11 @@ private Q_SLOTS: void onRemoveSearchDirectoryButtonClicked(); void onScanForNewGamesClicked(); void onRescanAllGamesClicked(); - void onUpdateRedumpDatabaseButtonClicked(); protected: void resizeEvent(QResizeEvent* event); private: - bool downloadRedumpDatabase(const QString& download_path); - QtHostInterface* m_host_interface; Ui::GameListSettingsWidget m_ui; diff --git a/src/duckstation-qt/gamelistsettingswidget.ui b/src/duckstation-qt/gamelistsettingswidget.ui index 62aa1effd..28fc9af65 100644 --- a/src/duckstation-qt/gamelistsettingswidget.ui +++ b/src/duckstation-qt/gamelistsettingswidget.ui @@ -121,23 +121,6 @@ - - - - - 0 - 0 - - - - Update Redump Database - - - - :/icons/applications-internet.png:/icons/applications-internet.png - - - diff --git a/src/duckstation-qt/gamelistwidget.cpp b/src/duckstation-qt/gamelistwidget.cpp index 2ec75d705..9064dbe65 100644 --- a/src/duckstation-qt/gamelistwidget.cpp +++ b/src/duckstation-qt/gamelistwidget.cpp @@ -278,7 +278,20 @@ void GameListWidget::resizeEvent(QResizeEvent* event) void GameListWidget::resizeTableViewColumnsToFit() { - QtUtils::ResizeColumnsForTableView(m_table_view, {32, 80, -1, -1, 100, 50, 100}); + QtUtils::ResizeColumnsForTableView(m_table_view, { + 32, // type + 80, // code + -1, // title + -1, // file title + 200, // developer + 200, // publisher + 200, // genre + 50, // year + 100, // players + 80, // size + 50, // region + 100 // compatibility + }); } static TinyString getColumnVisibilitySettingsKeyName(int column) @@ -288,8 +301,20 @@ static TinyString getColumnVisibilitySettingsKeyName(int column) void GameListWidget::loadTableViewColumnVisibilitySettings() { - static constexpr std::array DEFAULT_VISIBILITY = { - {true, true, true, false, true, true, true}}; + static constexpr std::array DEFAULT_VISIBILITY = {{ + true, // type + true, // code + true, // title + false, // file title + true, // developer + false, // publisher + false, // genre + true, // year + false, // players + true, // size + true, // region + true // compatibility + }}; for (int column = 0; column < GameListModel::Column_Count; column++) { diff --git a/src/duckstation-qt/gamepropertiesdialog.cpp b/src/duckstation-qt/gamepropertiesdialog.cpp index c6451f153..8eb56bec0 100644 --- a/src/duckstation-qt/gamepropertiesdialog.cpp +++ b/src/duckstation-qt/gamepropertiesdialog.cpp @@ -55,10 +55,23 @@ void GamePropertiesDialog::populate(const GameListEntry* ge) { const QString title_qstring(QString::fromStdString(ge->title)); + std::string hash_code; + std::unique_ptr cdi(CDImage::Open(ge->path.c_str(), nullptr)); + if (cdi) + { + hash_code = System::GetGameHashCodeForImage(cdi.get()); + cdi.reset(); + } + setWindowTitle(tr("Game Properties - %1").arg(title_qstring)); m_ui.imagePath->setText(QString::fromStdString(ge->path)); m_ui.title->setText(title_qstring); - m_ui.gameCode->setText(QString::fromStdString(ge->code)); + + if (!hash_code.empty() && ge->code != hash_code) + m_ui.gameCode->setText(QStringLiteral("%1 / %2").arg(ge->code.c_str()).arg(hash_code.c_str())); + else + m_ui.gameCode->setText(QString::fromStdString(ge->code)); + m_ui.region->setCurrentIndex(static_cast(ge->region)); if (ge->code.empty()) @@ -784,8 +797,8 @@ void GamePropertiesDialog::updateCPUClockSpeedLabel() void GamePropertiesDialog::fillEntryFromUi(GameListCompatibilityEntry* entry) { - entry->code = m_ui.gameCode->text().toStdString(); - entry->title = m_ui.title->text().toStdString(); + entry->code = m_game_code; + entry->title = m_game_title; entry->version_tested = m_ui.versionTested->text().toStdString(); entry->upscaling_issues = m_ui.upscalingIssues->text().toStdString(); entry->comments = m_ui.comments->text().toStdString(); @@ -795,7 +808,7 @@ void GamePropertiesDialog::fillEntryFromUi(GameListCompatibilityEntry* entry) void GamePropertiesDialog::saveCompatibilityInfo() { - if (m_ui.gameCode->text().isEmpty()) + if (m_game_code.empty()) return; GameListCompatibilityEntry new_entry; diff --git a/src/duckstation-qt/qtutils.cpp b/src/duckstation-qt/qtutils.cpp index 84c540f70..4675377df 100644 --- a/src/duckstation-qt/qtutils.cpp +++ b/src/duckstation-qt/qtutils.cpp @@ -58,21 +58,22 @@ ALWAYS_INLINE_RELEASE static void ResizeColumnsForView(T* view, const std::initi const int min_column_width = header->minimumSectionSize(); const int max_column_width = header->maximumSectionSize(); - const int total_width = - std::accumulate(widths.begin(), widths.end(), 0, [&min_column_width, &max_column_width](int a, int b) { - return a + ((b < 0) ? 0 : std::clamp(b, min_column_width, max_column_width)); - }); - const int scrollbar_width = ((view->verticalScrollBar() && view->verticalScrollBar()->isVisible()) || view->verticalScrollBarPolicy() == Qt::ScrollBarAlwaysOn) ? view->verticalScrollBar()->width() : 0; int num_flex_items = 0; + int total_width = 0; int column_index = 0; for (const int spec_width : widths) { - if (spec_width < 0 && !view->isColumnHidden(column_index)) - num_flex_items++; + if (!view->isColumnHidden(column_index)) + { + if (spec_width < 0) + num_flex_items++; + else + total_width += std::max(spec_width, min_column_width); + } column_index++; } @@ -91,7 +92,7 @@ ALWAYS_INLINE_RELEASE static void ResizeColumnsForView(T* view, const std::initi continue; } - const int width = spec_width < 0 ? flex_width : spec_width; + const int width = spec_width < 0 ? flex_width : (std::max(spec_width, min_column_width)); view->setColumnWidth(column_index, width); column_index++; } @@ -772,4 +773,4 @@ std::optional PromptForAddress(QWidget* parent, const QString& title, return address; } -} // namespace QtUtils \ No newline at end of file +} // namespace QtUtils diff --git a/src/duckstation-qt/settingsdialog.cpp b/src/duckstation-qt/settingsdialog.cpp index 337f9c0ac..4ca6774ce 100644 --- a/src/duckstation-qt/settingsdialog.cpp +++ b/src/duckstation-qt/settingsdialog.cpp @@ -93,10 +93,8 @@ void SettingsDialog::setCategoryHelpTexts() "console.

Mouse over an option for additional information."); m_category_help_text[static_cast(Category::GameListSettings)] = tr("Game List Settings
The list above shows the directories which will be searched by " - "DuckStation " - "to populate the game list. Search directories can be added, removed, and switched to recursive/non-recursive. " - "Additionally, the redump.org database can be downloaded or updated to provide titles for discs, as the discs " - "themselves do not provide title information."); + "DuckStation to populate the game list. Search directories can be added, removed, and switched to " + "recursive/non-recursive."); m_category_help_text[static_cast(Category::HotkeySettings)] = tr( "Hotkey Settings
Binding a hotkey allows you to trigger events such as a resetting or taking " "screenshots at the press of a key/controller button. Hotkey titles are self-explanatory. Clicking a binding will " diff --git a/src/frontend-common/CMakeLists.txt b/src/frontend-common/CMakeLists.txt index 621ecea4b..8f6f56422 100644 --- a/src/frontend-common/CMakeLists.txt +++ b/src/frontend-common/CMakeLists.txt @@ -9,6 +9,8 @@ add_library(frontend-common fullscreen_ui.h fullscreen_ui_progress_callback.cpp fullscreen_ui_progress_callback.h + game_database.cpp + game_database.h game_list.cpp game_list.h game_settings.cpp diff --git a/src/frontend-common/common_host_interface.cpp b/src/frontend-common/common_host_interface.cpp index c8c071584..80bbcc09d 100644 --- a/src/frontend-common/common_host_interface.cpp +++ b/src/frontend-common/common_host_interface.cpp @@ -85,7 +85,6 @@ bool CommonHostInterface::Initialize() m_game_list = std::make_unique(); m_game_list->SetCacheFilename(GetUserDirectoryRelativePath("cache/gamelist.cache")); - m_game_list->SetUserDatabaseFilename(GetUserDirectoryRelativePath("redump.dat")); m_game_list->SetUserCompatibilityListFilename(GetUserDirectoryRelativePath("compatibility.xml")); m_game_list->SetUserGameSettingsFilename(GetUserDirectoryRelativePath("gamesettings.ini")); @@ -2865,9 +2864,10 @@ void CommonHostInterface::GetGameInfo(const char* path, CDImage* image, std::str if (image) *code = System::GetGameCodeForImage(image, true); - const GameListDatabaseEntry* db_entry = (!code->empty()) ? m_game_list->GetDatabaseEntryForCode(*code) : nullptr; - if (db_entry) - *title = db_entry->title; + GameDatabase database; + GameDatabaseEntry database_entry; + if (database.Load() && database.GetEntryForDisc(image, &database_entry)) + *title = std::move(database_entry.title); else *title = System::GetTitleForPath(path); } @@ -2978,15 +2978,73 @@ bool CommonHostInterface::SaveScreenshot(const char* filename /* = nullptr */, b void CommonHostInterface::ApplyGameSettings(bool display_osd_messages) { + g_settings.controller_disable_analog_mode_forcing = false; + // this gets called while booting, so can't use valid if (System::IsShutdown() || System::GetRunningCode().empty() || !g_settings.apply_game_settings) return; + const GameListEntry* ge = m_game_list->GetEntryForPath(System::GetRunningPath().c_str()); + if (ge) + ApplyControllerCompatibilitySettings(ge->supported_controllers, display_osd_messages); + const GameSettings::Entry* gs = m_game_list->GetGameSettings(System::GetRunningPath(), System::GetRunningCode()); if (gs) gs->ApplySettings(display_osd_messages); } +void CommonHostInterface::ApplyControllerCompatibilitySettings(u64 controller_mask, bool display_osd_messages) +{ +#define BIT_FOR(ctype) (static_cast(1) << static_cast(ctype)) + + if (controller_mask == 0 || controller_mask == static_cast(-1)) + return; + + for (u32 i = 0; i < NUM_CONTROLLER_AND_CARD_PORTS; i++) + { + const ControllerType ctype = g_settings.controller_types[i]; + if (ctype == ControllerType::None) + continue; + + if (controller_mask & BIT_FOR(ctype)) + continue; + + // Special case: Dualshock is permitted when not supported as long as it's in digital mode. + if (ctype == ControllerType::AnalogController && + (controller_mask & BIT_FOR(ControllerType::DigitalController)) != 0) + { + g_settings.controller_disable_analog_mode_forcing = true; + continue; + } + + if (display_osd_messages) + { + SmallString supported_controller_string; + for (u32 j = 0; j < static_cast(ControllerType::Count); j++) + { + const ControllerType supported_ctype = static_cast(j); + if ((controller_mask & BIT_FOR(supported_ctype)) == 0) + continue; + + if (!supported_controller_string.IsEmpty()) + supported_controller_string.AppendString(", "); + + supported_controller_string.AppendString( + TranslateString("ControllerType", Settings::GetControllerTypeDisplayName(supported_ctype))); + } + + AddFormattedOSDMessage( + 30.0f, + TranslateString("OSDMessage", "Controller in port %u (%s) is not supported for %s.\nSupported controllers: " + "%s\nPlease configure a supported controller from the list above."), + i + 1u, TranslateString("ControllerType", Settings::GetControllerTypeDisplayName(ctype)).GetCharArray(), + System::GetRunningTitle().c_str(), supported_controller_string.GetCharArray()); + } + } + +#undef BIT_FOR +} + bool CommonHostInterface::UpdateControllerInputMapFromGameSettings() { // this gets called while booting, so can't use valid diff --git a/src/frontend-common/common_host_interface.h b/src/frontend-common/common_host_interface.h index 21816f8e9..8061f8720 100644 --- a/src/frontend-common/common_host_interface.h +++ b/src/frontend-common/common_host_interface.h @@ -15,6 +15,9 @@ class HostDisplayTexture; +class GameList; +struct GameDatabaseEntry; + class ControllerInterface; namespace FrontendCommon { @@ -411,6 +414,7 @@ protected: void RecreateSystem() override; void ApplyGameSettings(bool display_osd_messages); + void ApplyControllerCompatibilitySettings(u64 controller_mask, bool display_osd_messages); bool CreateHostDisplayResources(); void ReleaseHostDisplayResources(); diff --git a/src/frontend-common/frontend-common.vcxproj b/src/frontend-common/frontend-common.vcxproj index eb376ea23..b45997f00 100644 --- a/src/frontend-common/frontend-common.vcxproj +++ b/src/frontend-common/frontend-common.vcxproj @@ -93,6 +93,7 @@ + @@ -125,6 +126,7 @@ + diff --git a/src/frontend-common/frontend-common.vcxproj.filters b/src/frontend-common/frontend-common.vcxproj.filters index 54e7c39a0..4c52e58fb 100644 --- a/src/frontend-common/frontend-common.vcxproj.filters +++ b/src/frontend-common/frontend-common.vcxproj.filters @@ -31,6 +31,7 @@ + @@ -63,6 +64,7 @@ + diff --git a/src/frontend-common/fullscreen_ui.cpp b/src/frontend-common/fullscreen_ui.cpp index 6069b1dd7..5abc8b312 100644 --- a/src/frontend-common/fullscreen_ui.cpp +++ b/src/frontend-common/fullscreen_ui.cpp @@ -2871,6 +2871,14 @@ void DrawGameListWindow() ImGui::PushFont(g_medium_font); + // developer + if (!selected_entry->developer.empty()) + { + text_width = ImGui::CalcTextSize(selected_entry->developer.c_str(), nullptr, false, work_width).x; + ImGui::SetCursorPosX((work_width - text_width) / 2.0f); + ImGui::TextWrapped("%s", selected_entry->developer.c_str()); + } + // code text_width = ImGui::CalcTextSize(selected_entry->code.c_str(), nullptr, false, work_width).x; ImGui::SetCursorPosX((work_width - text_width) / 2.0f); @@ -2885,6 +2893,14 @@ void DrawGameListWindow() ImGui::SameLine(); ImGui::Text(" (%s)", Settings::GetDiscRegionDisplayName(selected_entry->region)); + // genre + ImGui::Text("Genre: %s", selected_entry->genre.c_str()); + + // release date + char release_date_str[64]; + selected_entry->GetReleaseDateString(release_date_str, sizeof(release_date_str)); + ImGui::Text("Release Date: %s", release_date_str); + // compatibility ImGui::TextUnformatted("Compatibility: "); ImGui::SameLine(); @@ -2896,9 +2912,6 @@ void DrawGameListWindow() // size ImGui::Text("Size: %.2f MB", static_cast(selected_entry->total_size) / 1048576.0f); - // TODO: last played - ImGui::Text("Last Played: Never"); - // game settings const u32 user_setting_count = selected_entry->settings.GetUserSettingsCount(); if (user_setting_count > 0) diff --git a/src/frontend-common/game_database.cpp b/src/frontend-common/game_database.cpp new file mode 100644 index 000000000..2ee0180bc --- /dev/null +++ b/src/frontend-common/game_database.cpp @@ -0,0 +1,225 @@ +#include "game_database.h" +#include "common/byte_stream.h" +#include "common/file_system.h" +#include "common/log.h" +#include "common/string_util.h" +#include "core/host_interface.h" +#include "core/system.h" +#include "rapidjson/document.h" +#include "rapidjson/error/en.h" +#include +Log_SetChannel(GameDatabase); + +GameDatabase::GameDatabase() = default; + +GameDatabase::~GameDatabase() +{ + Unload(); +} + +bool GameDatabase::Load() +{ + // TODO: use stream directly + std::unique_ptr stream( + g_host_interface->OpenPackageFile("database/gamedb.json", BYTESTREAM_OPEN_READ | BYTESTREAM_OPEN_STREAMED)); + if (!stream) + { + Log_ErrorPrintf("Failed to open game database"); + return false; + } + + std::string gamedb_data(FileSystem::ReadStreamToString(stream.get(), false)); + if (gamedb_data.empty()) + { + Log_ErrorPrintf("Failed to read game database"); + return false; + } + + std::unique_ptr json = std::make_unique(); + json->Parse(gamedb_data.c_str(), gamedb_data.size()); + if (json->HasParseError()) + { + Log_ErrorPrintf("Failed to parse game database: %s at offset %zu", + rapidjson::GetParseError_En(json->GetParseError()), json->GetErrorOffset()); + return false; + } + + if (!json->IsArray()) + { + Log_ErrorPrintf("Document is not an array"); + return false; + } + + m_json = json.release(); + return true; +} + +void GameDatabase::Unload() +{ + if (m_json) + { + delete static_cast(m_json); + m_json = nullptr; + } +} + +static bool GetStringFromObject(const rapidjson::Value& object, const char* key, std::string* dest) +{ + dest->clear(); + auto member = object.FindMember(key); + if (member == object.MemberEnd() || !member->value.IsString()) + return false; + + dest->assign(member->value.GetString(), member->value.GetStringLength()); + return true; +} + +static bool GetUIntFromObject(const rapidjson::Value& object, const char* key, u32* dest) +{ + *dest = 0; + + auto member = object.FindMember(key); + if (member == object.MemberEnd() || !member->value.IsUint()) + return false; + + *dest = member->value.GetUint(); + return true; +} + +static const rapidjson::Value* FindDatabaseEntry(const std::string_view& code, rapidjson::Document* json) +{ + for (const rapidjson::Value& current : json->GetArray()) + { + if (!current.IsObject()) + { + Log_WarningPrintf("entry is not an object"); + continue; + } + + auto member = current.FindMember("codes"); + if (member == current.MemberEnd()) + { + Log_WarningPrintf("codes member is missing"); + continue; + } + + if (!member->value.IsArray()) + { + Log_WarningPrintf("codes is not an array"); + continue; + } + + for (const rapidjson::Value& current_code : member->value.GetArray()) + { + if (!current_code.IsString()) + { + Log_WarningPrintf("code is not a string"); + continue; + } + + if (StringUtil::Strncasecmp(current_code.GetString(), code.data(), code.length()) == 0) + return ¤t; + } + } + + return nullptr; +} + +bool GameDatabase::GetEntryForCode(const std::string_view& code, GameDatabaseEntry* entry) +{ + if (!m_json) + return false; + + const rapidjson::Value* object = FindDatabaseEntry(code, static_cast(m_json)); + if (!object) + return false; + + if (!GetStringFromObject(*object, "serial", &entry->serial) || !GetStringFromObject(*object, "name", &entry->title)) + { + Log_ErrorPrintf("Missing serial or title for entry"); + return false; + } + + GetStringFromObject(*object, "genre", &entry->genre); + GetStringFromObject(*object, "developer", &entry->developer); + GetStringFromObject(*object, "publisher", &entry->publisher); + + GetUIntFromObject(*object, "minPlayers", &entry->min_players); + GetUIntFromObject(*object, "maxPlayers", &entry->max_players); + GetUIntFromObject(*object, "minBlocks", &entry->min_blocks); + GetUIntFromObject(*object, "maxBlocks", &entry->max_blocks); + + entry->release_date = 0; + { + std::string release_date; + if (GetStringFromObject(*object, "releaseDate", &release_date)) + { + std::istringstream iss(release_date); + struct tm parsed_time = {}; + iss >> std::get_time(&parsed_time, "%Y-%m-%d"); + if (!iss.fail()) + { + parsed_time.tm_isdst = 0; +#ifdef _WIN32 + entry->release_date = _mkgmtime(&parsed_time); +#else + entry->release_date = timegm(&parsed_time); +#endif + } + } + } + + entry->supported_controllers_mask = ~0u; + auto controllers = object->FindMember("controllers"); + if (controllers != object->MemberEnd()) + { + if (controllers->value.IsArray()) + { + bool first = true; + for (const rapidjson::Value& controller : controllers->value.GetArray()) + { + if (!controller.IsString()) + { + Log_WarningPrintf("controller is not a string"); + return false; + } + + std::optional ctype = Settings::ParseControllerTypeName(controller.GetString()); + if (!ctype.has_value()) + { + Log_WarningPrintf("Invalid controller type '%s'", controller.GetString()); + return false; + } + + if (first) + { + entry->supported_controllers_mask = 0; + first = false; + } + + entry->supported_controllers_mask |= (1u << static_cast(ctype.value())); + } + } + else + { + Log_WarningPrintf("controllers is not an array"); + } + } + + return true; +} + +bool GameDatabase::GetEntryForDisc(CDImage* image, GameDatabaseEntry* entry) +{ + std::string exe_name_code(System::GetGameCodeForImage(image, false)); + if (!exe_name_code.empty() && GetEntryForCode(exe_name_code, entry)) + return true; + + std::string exe_hash_code(System::GetGameHashCodeForImage(image)); + if (!exe_hash_code.empty() && GetEntryForCode(exe_hash_code, entry)) + return true; + + Log_WarningPrintf("No entry found for disc (exe code: '%s', hash code: '%s')", exe_name_code.c_str(), + exe_hash_code.c_str()); + return false; +} diff --git a/src/frontend-common/game_database.h b/src/frontend-common/game_database.h new file mode 100644 index 000000000..2884bfbde --- /dev/null +++ b/src/frontend-common/game_database.h @@ -0,0 +1,45 @@ +#pragma once +#include "core/types.h" +#include +#include +#include +#include +#include +#include + +class CDImage; + +struct GameDatabaseEntry +{ + std::string serial; + std::string title; + std::string genre; + std::string developer; + std::string publisher; + u64 release_date; + u32 min_players; + u32 max_players; + u32 min_blocks; + u32 max_blocks; + u32 supported_controllers_mask; +}; + +class GameDatabase +{ +public: + GameDatabase(); + ~GameDatabase(); + + bool Load(); + void Unload(); + + bool GetEntryForDisc(CDImage* image, GameDatabaseEntry* entry); + + bool GetEntryForCode(const std::string_view& code, GameDatabaseEntry* entry); + + bool GetTitleAndSerialForDisc(CDImage* image, GameDatabaseEntry* entry); + //bool Get + +private: + void* m_json = nullptr; +}; diff --git a/src/frontend-common/game_list.cpp b/src/frontend-common/game_list.cpp index e3f4df460..8e54adaba 100644 --- a/src/frontend-common/game_list.cpp +++ b/src/frontend-common/game_list.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -158,40 +159,50 @@ bool GameList::GetGameListEntry(const std::string& path, GameListEntry* entry) if (!cdi) return false; - std::string code = System::GetGameCodeForImage(cdi.get(), true); - DiscRegion region = System::GetRegionFromSystemArea(cdi.get()); - if (region == DiscRegion::Other) - region = System::GetRegionForCode(code); - entry->path = path; - entry->code = std::move(code); - entry->region = region; entry->total_size = static_cast(CDImage::RAW_SECTOR_SIZE) * static_cast(cdi->GetLBACount()); entry->type = GameListEntryType::Disc; entry->compatibility_rating = GameListCompatibilityRating::Unknown; - if (entry->code.empty()) + // try the database first + LoadDatabase(); + GameDatabaseEntry dbentry; + if (!m_database.GetEntryForDisc(cdi.get(), &dbentry)) { // no game code, so use the filename title + entry->code = System::GetGameCodeForImage(cdi.get(), true); entry->title = System::GetTitleForPath(path.c_str()); entry->compatibility_rating = GameListCompatibilityRating::Unknown; + entry->release_date = 0; + entry->min_players = 0; + entry->max_players = 0; + entry->min_blocks = 0; + entry->max_blocks = 0; + entry->supported_controllers = ~0u; } else { - const GameListDatabaseEntry* database_entry = GetDatabaseEntryForCode(entry->code); - if (database_entry) - { - entry->title = database_entry->title; + // pull from database + entry->code = std::move(dbentry.serial); + entry->title = std::move(dbentry.title); + entry->genre = std::move(dbentry.genre); + entry->publisher = std::move(dbentry.publisher); + entry->developer = std::move(dbentry.developer); + entry->release_date = dbentry.release_date; + entry->min_players = dbentry.min_players; + entry->max_players = dbentry.max_players; + entry->min_blocks = dbentry.min_blocks; + entry->max_blocks = dbentry.max_blocks; + entry->supported_controllers = dbentry.supported_controllers_mask; + } - if (entry->region != database_entry->region) - Log_WarningPrintf("Region mismatch between disc and database for '%s'", entry->code.c_str()); - } - else - { - Log_WarningPrintf("'%s' not found in database", entry->code.c_str()); - entry->title = System::GetTitleForPath(path.c_str()); - } + // region detection + entry->region = System::GetRegionFromSystemArea(cdi.get()); + if (entry->region == DiscRegion::Other) + entry->region = System::GetRegionForCode(entry->code); + if (!entry->code.empty()) + { const GameListCompatibilityEntry* compatibility_entry = GetCompatibilityEntryForCode(entry->code); if (compatibility_entry) entry->compatibility_rating = compatibility_entry->compatibility_rating; @@ -328,30 +339,27 @@ bool GameList::LoadEntriesFromCache(ByteStream* stream) while (stream->GetPosition() != stream->GetSize()) { std::string path; - std::string code; - std::string title; - u64 total_size; - u64 last_modified_time; - u8 region; + GameListEntry ge; + u8 type; + u8 region; u8 compatibility_rating; - if (!ReadString(stream, &path) || !ReadString(stream, &code) || !ReadString(stream, &title) || - !ReadU64(stream, &total_size) || !ReadU64(stream, &last_modified_time) || !ReadU8(stream, ®ion) || - region >= static_cast(DiscRegion::Count) || !ReadU8(stream, &type) || - type >= static_cast(GameListEntryType::Count) || !ReadU8(stream, &compatibility_rating) || + if (!ReadU8(stream, &type) || !ReadU8(stream, ®ion) || !ReadString(stream, &path) || + !ReadString(stream, &ge.code) || !ReadString(stream, &ge.title) || !ReadString(stream, &ge.genre) || + !ReadString(stream, &ge.publisher) || !ReadString(stream, &ge.developer) || !ReadU64(stream, &ge.total_size) || + !ReadU64(stream, &ge.last_modified_time) || !ReadU64(stream, &ge.release_date) || + !ReadU32(stream, &ge.supported_controllers) || !ReadU8(stream, &ge.min_players) || + !ReadU8(stream, &ge.max_players) || !ReadU8(stream, &ge.min_blocks) || !ReadU8(stream, &ge.max_blocks) || + !ReadU8(stream, &compatibility_rating) || region >= static_cast(DiscRegion::Count) || + type >= static_cast(GameListEntryType::Count) || compatibility_rating >= static_cast(GameListCompatibilityRating::Count)) { Log_WarningPrintf("Game list cache entry is corrupted"); return false; } - GameListEntry ge; ge.path = path; - ge.code = std::move(code); - ge.title = std::move(title); - ge.total_size = total_size; - ge.last_modified_time = last_modified_time; ge.region = static_cast(region); ge.type = static_cast(type); ge.compatibility_rating = static_cast(compatibility_rating); @@ -405,13 +413,23 @@ bool GameList::OpenCacheForWriting() bool GameList::WriteEntryToCache(const GameListEntry* entry, ByteStream* stream) { - bool result = WriteString(stream, entry->path); + bool result = true; + result &= WriteU8(stream, static_cast(entry->type)); + result &= WriteU8(stream, static_cast(entry->region)); + result &= WriteString(stream, entry->path); result &= WriteString(stream, entry->code); result &= WriteString(stream, entry->title); + result &= WriteString(stream, entry->genre); + result &= WriteString(stream, entry->publisher); + result &= WriteString(stream, entry->developer); result &= WriteU64(stream, entry->total_size); result &= WriteU64(stream, entry->last_modified_time); - result &= WriteU8(stream, static_cast(entry->region)); - result &= WriteU8(stream, static_cast(entry->type)); + result &= WriteU64(stream, entry->release_date); + result &= WriteU32(stream, entry->supported_controllers); + result &= WriteU8(stream, entry->min_players); + result &= WriteU8(stream, entry->max_players); + result &= WriteU8(stream, entry->min_blocks); + result &= WriteU8(stream, entry->max_blocks); result &= WriteU8(stream, static_cast(entry->compatibility_rating)); result &= entry->settings.SaveToStream(stream); return result; @@ -524,84 +542,6 @@ void GameList::ScanDirectory(const char* path, bool recursive, ProgressCallback* progress->PopState(); } -class GameList::RedumpDatVisitor final : public tinyxml2::XMLVisitor -{ -public: - RedumpDatVisitor(DatabaseMap& database) : m_database(database) {} - - static std::string FixupSerial(const std::string_view str) - { - std::string ret; - ret.reserve(str.length()); - for (size_t i = 0; i < str.length(); i++) - { - if (str[i] == '.' || str[i] == '#') - continue; - else if (str[i] == ',') - break; - else if (str[i] == '_' || str[i] == ' ') - ret.push_back('-'); - else - ret.push_back(static_cast(std::toupper(str[i]))); - } - - return ret; - } - - bool VisitEnter(const tinyxml2::XMLElement& element, const tinyxml2::XMLAttribute* firstAttribute) override - { - // recurse into gamelist - if (StringUtil::Strcasecmp(element.Name(), "datafile") == 0) - return true; - - if (StringUtil::Strcasecmp(element.Name(), "game") != 0) - return false; - - const char* name = element.Attribute("name"); - if (!name) - return false; - - const tinyxml2::XMLElement* serial_elem = element.FirstChildElement("serial"); - if (!serial_elem) - return false; - - const char* serial_text = serial_elem->GetText(); - if (!serial_text) - return false; - - // Handle entries like SCES-00984, SCES-00984# - const char* start = serial_text; - const char* end = std::strchr(start, ','); - for (;;) - { - std::string code = FixupSerial(end ? std::string_view(start, end - start) : std::string_view(start)); - auto iter = m_database.find(code); - if (iter == m_database.end()) - { - GameListDatabaseEntry gde; - gde.code = std::move(code); - gde.region = System::GetRegionForCode(gde.code); - gde.title = name; - m_database.emplace(gde.code, std::move(gde)); - } - - if (!end) - break; - - start = end + 1; - while (std::isspace(*start)) - start++; - - end = std::strchr(start, ','); - } - - return false; - } - -private: - DatabaseMap& m_database; -}; - void GameList::AddDirectory(std::string path, bool recursive) { auto iter = std::find_if(m_search_directories.begin(), m_search_directories.end(), @@ -639,15 +579,6 @@ GameListEntry* GameList::GetMutableEntryForPath(const char* path) return nullptr; } -const GameListDatabaseEntry* GameList::GetDatabaseEntryForCode(const std::string& code) const -{ - if (!m_database_load_tried) - const_cast(this)->LoadDatabase(); - - auto iter = m_database.find(code); - return (iter != m_database.end()) ? &iter->second : nullptr; -} - const GameListCompatibilityEntry* GameList::GetCompatibilityEntryForCode(const std::string& code) const { if (!m_compatibility_list_load_tried) @@ -734,54 +665,12 @@ void GameList::LoadDatabase() return; m_database_load_tried = true; - - tinyxml2::XMLDocument doc; - if (FileSystem::FileExists(m_user_database_filename.c_str())) - { - std::unique_ptr stream = - FileSystem::OpenFile(m_user_database_filename.c_str(), BYTESTREAM_OPEN_READ | BYTESTREAM_OPEN_STREAMED); - if (stream) - { - const std::string xml(FileSystem::ReadStreamToString(stream.get())); - tinyxml2::XMLError error = doc.Parse(xml.data(), xml.size()); - if (error != tinyxml2::XML_SUCCESS) - { - Log_ErrorPrintf("Failed to parse redump dat: %s", tinyxml2::XMLDocument::ErrorIDToName(error)); - doc.Clear(); - } - } - } - if (!doc.RootElement()) - { - std::unique_ptr stream = g_host_interface->OpenPackageFile( - "database" FS_OSPATH_SEPARATOR_STR "redump.dat", BYTESTREAM_OPEN_READ | BYTESTREAM_OPEN_STREAMED); - if (stream) - { - const std::string xml(FileSystem::ReadStreamToString(stream.get())); - tinyxml2::XMLError error = doc.Parse(xml.data(), xml.size()); - if (error != tinyxml2::XML_SUCCESS) - { - Log_ErrorPrintf("Failed to parse redump dat: %s", tinyxml2::XMLDocument::ErrorIDToName(error)); - return; - } - } - } - - const tinyxml2::XMLElement* datafile_elem = doc.FirstChildElement("datafile"); - if (!datafile_elem) - { - Log_ErrorPrintf("Failed to get datafile element in redump dat"); - return; - } - - RedumpDatVisitor visitor(m_database); - datafile_elem->Accept(&visitor); - Log_InfoPrintf("Loaded %zu entries from Redump.org database", m_database.size()); + m_database.Load(); } void GameList::ClearDatabase() { - m_database.clear(); + m_database.Unload(); m_database_load_tried = false; } @@ -1205,3 +1094,20 @@ std::string GameList::GetNewCoverImagePathForEntry(const GameListEntry* entry, c return g_host_interface->GetUserDirectoryRelativePath("covers" FS_OSPATH_SEPARATOR_STR "%s%s", entry->title.c_str(), extension); } + +size_t GameListEntry::GetReleaseDateString(char* buffer, size_t buffer_size) const +{ + if (release_date == 0) + return StringUtil::Strlcpy(buffer, "Unknown", buffer_size); + + std::time_t date_as_time = static_cast(release_date); +#ifdef _WIN32 + tm date_tm = {}; + gmtime_s(&date_tm, &date_as_time); +#else + tm date_tm = {}; + gmtime_r(&date_as_time, &date_tm); +#endif + + return std::strftime(buffer, buffer_size, "%d %B %Y", &date_tm); +} diff --git a/src/frontend-common/game_list.h b/src/frontend-common/game_list.h index 8b5805604..361d6d969 100644 --- a/src/frontend-common/game_list.h +++ b/src/frontend-common/game_list.h @@ -1,6 +1,7 @@ #pragma once #include "core/types.h" #include "game_settings.h" +#include "game_database.h" #include #include #include @@ -34,24 +35,31 @@ enum class GameListCompatibilityRating Count, }; -struct GameListDatabaseEntry +struct GameListEntry { - std::string code; - std::string title; + GameListEntryType type; DiscRegion region; -}; -struct GameListEntry -{ std::string path; std::string code; std::string title; + std::string genre; + std::string publisher; + std::string developer; u64 total_size; u64 last_modified_time; - DiscRegion region; - GameListEntryType type; + + u64 release_date; + u32 supported_controllers; + u8 min_players; + u8 max_players; + u8 min_blocks; + u8 max_blocks; + GameListCompatibilityRating compatibility_rating; GameSettings::Entry settings; + + size_t GetReleaseDateString(char* buffer, size_t buffer_size) const; }; struct GameListCompatibilityEntry @@ -93,11 +101,11 @@ public: const u32 GetSearchDirectoryCount() const { return static_cast(m_search_directories.size()); } const GameListEntry* GetEntryForPath(const char* path) const; - const GameListDatabaseEntry* GetDatabaseEntryForCode(const std::string& code) const; const GameListCompatibilityEntry* GetCompatibilityEntryForCode(const std::string& code) const; + + bool GetGameCodeAndTitleFromDatabase(const char* path, std::string* code, std::string* title); void SetCacheFilename(std::string filename) { m_cache_filename = std::move(filename); } - void SetUserDatabaseFilename(std::string filename) { m_user_database_filename = std::move(filename); } void SetUserCompatibilityListFilename(std::string filename) { m_user_compatibility_list_filename = std::move(filename); @@ -123,10 +131,9 @@ private: enum : u32 { GAME_LIST_CACHE_SIGNATURE = 0x45434C47, - GAME_LIST_CACHE_VERSION = 25 + GAME_LIST_CACHE_VERSION = 26 }; - using DatabaseMap = std::unordered_map; using CacheMap = std::unordered_map; using CompatibilityMap = std::unordered_map; @@ -160,16 +167,15 @@ private: void LoadGameSettings(); - DatabaseMap m_database; EntryList m_entries; CacheMap m_cache_map; + GameDatabase m_database; CompatibilityMap m_compatibility_list; GameSettings::Database m_game_settings; std::unique_ptr m_cache_write_stream; std::vector m_search_directories; std::string m_cache_filename; - std::string m_user_database_filename; std::string m_user_compatibility_list_filename; std::string m_user_game_settings_filename; bool m_database_load_tried = false; diff --git a/src/frontend-common/game_settings.cpp b/src/frontend-common/game_settings.cpp index 7feb48c7e..dbd075be8 100644 --- a/src/frontend-common/game_settings.cpp +++ b/src/frontend-common/game_settings.cpp @@ -33,7 +33,6 @@ std::array, static_cast(Trait::Count)> {"DisablePGXPDepthBuffer", TRANSLATABLE("GameSettingsTrait", "Disable PGXP Depth Buffer")}, {"ForcePGXPVertexCache", TRANSLATABLE("GameSettingsTrait", "Force PGXP Vertex Cache")}, {"ForcePGXPCPUMode", TRANSLATABLE("GameSettingsTrait", "Force PGXP CPU Mode")}, - {"DisableAnalogModeForcing", TRANSLATABLE("GameSettingsTrait", "Disable Forcing Controller Analog Mode on Reset")}, {"ForceRecompilerMemoryExceptions", TRANSLATABLE("GameSettingsTrait", "Force Recompiler Memory Exceptions")}, {"ForceRecompilerICache", TRANSLATABLE("GameSettingsTrait", "Force Recompiler ICache")}, }}; @@ -1240,9 +1239,6 @@ void Entry::ApplySettings(bool display_osd_messages) const g_settings.gpu_pgxp_depth_buffer = false; } - if (HasTrait(Trait::DisableAnalogModeForcing)) - g_settings.controller_disable_analog_mode_forcing = true; - if (HasTrait(Trait::ForceRecompilerMemoryExceptions)) g_settings.cpu_recompiler_memory_exceptions = true; diff --git a/src/frontend-common/game_settings.h b/src/frontend-common/game_settings.h index dc08e5d75..efe155541 100644 --- a/src/frontend-common/game_settings.h +++ b/src/frontend-common/game_settings.h @@ -25,7 +25,6 @@ enum class Trait : u32 DisablePGXPDepthBuffer, ForcePGXPVertexCache, ForcePGXPCPUMode, - DisableAnalogModeForcing, ForceRecompilerMemoryExceptions, ForceRecompilerICache,