mirror of
https://github.com/RetroDECK/Duckstation.git
synced 2024-11-22 05:45:38 +00:00
Add JSON game database to replace dat parsing
This commit is contained in:
parent
b25030b19a
commit
ff14e8aede
20
scripts/merge_gamedb.py
Normal file
20
scripts/merge_gamedb.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if (len(sys.argv) < 3):
|
||||||
|
print("usage: %s <gamedb dir> <output path>" % 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])
|
|
@ -414,6 +414,11 @@ std::string GetGameCodeForImage(CDImage* cdi, bool fallback_to_hash)
|
||||||
if (!fallback_to_hash)
|
if (!fallback_to_hash)
|
||||||
return {};
|
return {};
|
||||||
|
|
||||||
|
return GetGameHashCodeForImage(cdi);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string GetGameHashCodeForImage(CDImage* cdi)
|
||||||
|
{
|
||||||
std::string exe_name;
|
std::string exe_name;
|
||||||
std::vector<u8> exe_buffer;
|
std::vector<u8> exe_buffer;
|
||||||
if (!ReadExecutableFromImage(cdi, &exe_name, &exe_buffer))
|
if (!ReadExecutableFromImage(cdi, &exe_name, &exe_buffer))
|
||||||
|
|
|
@ -72,6 +72,7 @@ ConsoleRegion GetConsoleRegionForDiscRegion(DiscRegion region);
|
||||||
std::string GetExecutableNameForImage(CDImage* cdi);
|
std::string GetExecutableNameForImage(CDImage* cdi);
|
||||||
bool ReadExecutableFromImage(CDImage* cdi, std::string* out_executable_name, std::vector<u8>* out_executable_data);
|
bool ReadExecutableFromImage(CDImage* cdi, std::string* out_executable_name, std::vector<u8>* out_executable_data);
|
||||||
|
|
||||||
|
std::string GetGameHashCodeForImage(CDImage* cdi);
|
||||||
std::string GetGameCodeForImage(CDImage* cdi, bool fallback_to_hash);
|
std::string GetGameCodeForImage(CDImage* cdi, bool fallback_to_hash);
|
||||||
std::string GetGameCodeForPath(const char* image_path, bool fallback_to_hash);
|
std::string GetGameCodeForPath(const char* image_path, bool fallback_to_hash);
|
||||||
DiscRegion GetRegionForCode(std::string_view code);
|
DiscRegion GetRegionForCode(std::string_view code);
|
||||||
|
|
|
@ -2,11 +2,14 @@
|
||||||
#include "common/file_system.h"
|
#include "common/file_system.h"
|
||||||
#include "common/string_util.h"
|
#include "common/string_util.h"
|
||||||
#include "core/system.h"
|
#include "core/system.h"
|
||||||
|
#include <QtCore/QDate>
|
||||||
|
#include <QtCore/QDateTime>
|
||||||
#include <QtGui/QIcon>
|
#include <QtGui/QIcon>
|
||||||
#include <QtGui/QPainter>
|
#include <QtGui/QPainter>
|
||||||
|
|
||||||
static constexpr std::array<const char*, GameListModel::Column_Count> s_column_names = {
|
static constexpr std::array<const char*, GameListModel::Column_Count> 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_WIDTH = 512;
|
||||||
static constexpr int COVER_ART_HEIGHT = 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<int>(file_title.length()));
|
return QString::fromUtf8(file_title.data(), static_cast<int>(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<qint64>(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:
|
case Column_Size:
|
||||||
return QString("%1 MB").arg(static_cast<double>(ge.total_size) / 1048576.0, 0, 'f', 2);
|
return QString("%1 MB").arg(static_cast<double>(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<int>(file_title.length()));
|
return QString::fromUtf8(file_title.data(), static_cast<int>(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<qint64>(ge.release_date), Qt::UTC).date().year();
|
||||||
|
|
||||||
|
case Column_Players:
|
||||||
|
return static_cast<int>(ge.max_players);
|
||||||
|
|
||||||
case Column_Region:
|
case Column_Region:
|
||||||
return static_cast<int>(ge.region);
|
return static_cast<int>(ge.region);
|
||||||
|
|
||||||
|
@ -422,6 +470,11 @@ void GameListModel::setColumnDisplayNames()
|
||||||
m_column_display_names[Column_Code] = tr("Code");
|
m_column_display_names[Column_Code] = tr("Code");
|
||||||
m_column_display_names[Column_Title] = tr("Title");
|
m_column_display_names[Column_Title] = tr("Title");
|
||||||
m_column_display_names[Column_FileTitle] = tr("File 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_Size] = tr("Size");
|
||||||
m_column_display_names[Column_Region] = tr("Region");
|
m_column_display_names[Column_Region] = tr("Region");
|
||||||
m_column_display_names[Column_Compatibility] = tr("Compatibility");
|
m_column_display_names[Column_Compatibility] = tr("Compatibility");
|
||||||
|
|
|
@ -19,6 +19,11 @@ public:
|
||||||
Column_Code,
|
Column_Code,
|
||||||
Column_Title,
|
Column_Title,
|
||||||
Column_FileTitle,
|
Column_FileTitle,
|
||||||
|
Column_Developer,
|
||||||
|
Column_Publisher,
|
||||||
|
Column_Genre,
|
||||||
|
Column_Year,
|
||||||
|
Column_Players,
|
||||||
Column_Size,
|
Column_Size,
|
||||||
Column_Region,
|
Column_Region,
|
||||||
Column_Compatibility,
|
Column_Compatibility,
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
#include "gamelistsettingswidget.h"
|
#include "gamelistsettingswidget.h"
|
||||||
#include "common/assert.h"
|
#include "common/assert.h"
|
||||||
#include "common/file_system.h"
|
#include "common/file_system.h"
|
||||||
#include "common/minizip_helpers.h"
|
|
||||||
#include "common/string_util.h"
|
#include "common/string_util.h"
|
||||||
#include "frontend-common/game_list.h"
|
#include "frontend-common/game_list.h"
|
||||||
#include "gamelistsearchdirectoriesmodel.h"
|
#include "gamelistsearchdirectoriesmodel.h"
|
||||||
|
@ -11,18 +10,12 @@
|
||||||
#include <QtCore/QDebug>
|
#include <QtCore/QDebug>
|
||||||
#include <QtCore/QSettings>
|
#include <QtCore/QSettings>
|
||||||
#include <QtCore/QUrl>
|
#include <QtCore/QUrl>
|
||||||
#include <QtNetwork/QNetworkAccessManager>
|
|
||||||
#include <QtNetwork/QNetworkReply>
|
|
||||||
#include <QtNetwork/QNetworkRequest>
|
|
||||||
#include <QtWidgets/QFileDialog>
|
#include <QtWidgets/QFileDialog>
|
||||||
#include <QtWidgets/QHeaderView>
|
#include <QtWidgets/QHeaderView>
|
||||||
#include <QtWidgets/QMenu>
|
#include <QtWidgets/QMenu>
|
||||||
#include <QtWidgets/QMessageBox>
|
#include <QtWidgets/QMessageBox>
|
||||||
#include <QtWidgets/QProgressDialog>
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
static constexpr char REDUMP_DOWNLOAD_URL[] = "http://redump.org/datfile/psx/serial,version,description";
|
|
||||||
|
|
||||||
GameListSettingsWidget::GameListSettingsWidget(QtHostInterface* host_interface, QWidget* parent /* = nullptr */)
|
GameListSettingsWidget::GameListSettingsWidget(QtHostInterface* host_interface, QWidget* parent /* = nullptr */)
|
||||||
: QWidget(parent), m_host_interface(host_interface)
|
: QWidget(parent), m_host_interface(host_interface)
|
||||||
{
|
{
|
||||||
|
@ -48,8 +41,6 @@ GameListSettingsWidget::GameListSettingsWidget(QtHostInterface* host_interface,
|
||||||
&GameListSettingsWidget::onRemoveSearchDirectoryButtonClicked);
|
&GameListSettingsWidget::onRemoveSearchDirectoryButtonClicked);
|
||||||
connect(m_ui.rescanAllGames, &QPushButton::clicked, this, &GameListSettingsWidget::onRescanAllGamesClicked);
|
connect(m_ui.rescanAllGames, &QPushButton::clicked, this, &GameListSettingsWidget::onRescanAllGamesClicked);
|
||||||
connect(m_ui.scanForNewGames, &QPushButton::clicked, this, &GameListSettingsWidget::onScanForNewGamesClicked);
|
connect(m_ui.scanForNewGames, &QPushButton::clicked, this, &GameListSettingsWidget::onScanForNewGamesClicked);
|
||||||
connect(m_ui.updateRedumpDatabase, &QPushButton::clicked, this,
|
|
||||||
&GameListSettingsWidget::onUpdateRedumpDatabaseButtonClicked);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
GameListSettingsWidget::~GameListSettingsWidget() = default;
|
GameListSettingsWidget::~GameListSettingsWidget() = default;
|
||||||
|
@ -135,156 +126,3 @@ void GameListSettingsWidget::onScanForNewGamesClicked()
|
||||||
{
|
{
|
||||||
m_host_interface->refreshGameList(false, false);
|
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<int>(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<int>(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<int>(total));
|
|
||||||
progress.setValue(static_cast<int>(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);
|
|
||||||
}
|
|
||||||
|
|
|
@ -26,14 +26,11 @@ private Q_SLOTS:
|
||||||
void onRemoveSearchDirectoryButtonClicked();
|
void onRemoveSearchDirectoryButtonClicked();
|
||||||
void onScanForNewGamesClicked();
|
void onScanForNewGamesClicked();
|
||||||
void onRescanAllGamesClicked();
|
void onRescanAllGamesClicked();
|
||||||
void onUpdateRedumpDatabaseButtonClicked();
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void resizeEvent(QResizeEvent* event);
|
void resizeEvent(QResizeEvent* event);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool downloadRedumpDatabase(const QString& download_path);
|
|
||||||
|
|
||||||
QtHostInterface* m_host_interface;
|
QtHostInterface* m_host_interface;
|
||||||
|
|
||||||
Ui::GameListSettingsWidget m_ui;
|
Ui::GameListSettingsWidget m_ui;
|
||||||
|
|
|
@ -121,23 +121,6 @@
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
|
||||||
<widget class="QPushButton" name="updateRedumpDatabase">
|
|
||||||
<property name="sizePolicy">
|
|
||||||
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
|
|
||||||
<horstretch>0</horstretch>
|
|
||||||
<verstretch>0</verstretch>
|
|
||||||
</sizepolicy>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Update Redump Database</string>
|
|
||||||
</property>
|
|
||||||
<property name="icon">
|
|
||||||
<iconset resource="resources/resources.qrc">
|
|
||||||
<normaloff>:/icons/applications-internet.png</normaloff>:/icons/applications-internet.png</iconset>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
|
|
|
@ -278,7 +278,20 @@ void GameListWidget::resizeEvent(QResizeEvent* event)
|
||||||
|
|
||||||
void GameListWidget::resizeTableViewColumnsToFit()
|
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)
|
static TinyString getColumnVisibilitySettingsKeyName(int column)
|
||||||
|
@ -288,8 +301,20 @@ static TinyString getColumnVisibilitySettingsKeyName(int column)
|
||||||
|
|
||||||
void GameListWidget::loadTableViewColumnVisibilitySettings()
|
void GameListWidget::loadTableViewColumnVisibilitySettings()
|
||||||
{
|
{
|
||||||
static constexpr std::array<bool, GameListModel::Column_Count> DEFAULT_VISIBILITY = {
|
static constexpr std::array<bool, GameListModel::Column_Count> DEFAULT_VISIBILITY = {{
|
||||||
{true, true, true, false, true, true, true}};
|
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++)
|
for (int column = 0; column < GameListModel::Column_Count; column++)
|
||||||
{
|
{
|
||||||
|
|
|
@ -55,10 +55,23 @@ void GamePropertiesDialog::populate(const GameListEntry* ge)
|
||||||
{
|
{
|
||||||
const QString title_qstring(QString::fromStdString(ge->title));
|
const QString title_qstring(QString::fromStdString(ge->title));
|
||||||
|
|
||||||
|
std::string hash_code;
|
||||||
|
std::unique_ptr<CDImage> 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));
|
setWindowTitle(tr("Game Properties - %1").arg(title_qstring));
|
||||||
m_ui.imagePath->setText(QString::fromStdString(ge->path));
|
m_ui.imagePath->setText(QString::fromStdString(ge->path));
|
||||||
m_ui.title->setText(title_qstring);
|
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<int>(ge->region));
|
m_ui.region->setCurrentIndex(static_cast<int>(ge->region));
|
||||||
|
|
||||||
if (ge->code.empty())
|
if (ge->code.empty())
|
||||||
|
@ -784,8 +797,8 @@ void GamePropertiesDialog::updateCPUClockSpeedLabel()
|
||||||
|
|
||||||
void GamePropertiesDialog::fillEntryFromUi(GameListCompatibilityEntry* entry)
|
void GamePropertiesDialog::fillEntryFromUi(GameListCompatibilityEntry* entry)
|
||||||
{
|
{
|
||||||
entry->code = m_ui.gameCode->text().toStdString();
|
entry->code = m_game_code;
|
||||||
entry->title = m_ui.title->text().toStdString();
|
entry->title = m_game_title;
|
||||||
entry->version_tested = m_ui.versionTested->text().toStdString();
|
entry->version_tested = m_ui.versionTested->text().toStdString();
|
||||||
entry->upscaling_issues = m_ui.upscalingIssues->text().toStdString();
|
entry->upscaling_issues = m_ui.upscalingIssues->text().toStdString();
|
||||||
entry->comments = m_ui.comments->text().toStdString();
|
entry->comments = m_ui.comments->text().toStdString();
|
||||||
|
@ -795,7 +808,7 @@ void GamePropertiesDialog::fillEntryFromUi(GameListCompatibilityEntry* entry)
|
||||||
|
|
||||||
void GamePropertiesDialog::saveCompatibilityInfo()
|
void GamePropertiesDialog::saveCompatibilityInfo()
|
||||||
{
|
{
|
||||||
if (m_ui.gameCode->text().isEmpty())
|
if (m_game_code.empty())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
GameListCompatibilityEntry new_entry;
|
GameListCompatibilityEntry new_entry;
|
||||||
|
|
|
@ -58,21 +58,22 @@ ALWAYS_INLINE_RELEASE static void ResizeColumnsForView(T* view, const std::initi
|
||||||
|
|
||||||
const int min_column_width = header->minimumSectionSize();
|
const int min_column_width = header->minimumSectionSize();
|
||||||
const int max_column_width = header->maximumSectionSize();
|
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()) ||
|
const int scrollbar_width = ((view->verticalScrollBar() && view->verticalScrollBar()->isVisible()) ||
|
||||||
view->verticalScrollBarPolicy() == Qt::ScrollBarAlwaysOn) ?
|
view->verticalScrollBarPolicy() == Qt::ScrollBarAlwaysOn) ?
|
||||||
view->verticalScrollBar()->width() :
|
view->verticalScrollBar()->width() :
|
||||||
0;
|
0;
|
||||||
int num_flex_items = 0;
|
int num_flex_items = 0;
|
||||||
|
int total_width = 0;
|
||||||
int column_index = 0;
|
int column_index = 0;
|
||||||
for (const int spec_width : widths)
|
for (const int spec_width : widths)
|
||||||
{
|
{
|
||||||
if (spec_width < 0 && !view->isColumnHidden(column_index))
|
if (!view->isColumnHidden(column_index))
|
||||||
num_flex_items++;
|
{
|
||||||
|
if (spec_width < 0)
|
||||||
|
num_flex_items++;
|
||||||
|
else
|
||||||
|
total_width += std::max(spec_width, min_column_width);
|
||||||
|
}
|
||||||
|
|
||||||
column_index++;
|
column_index++;
|
||||||
}
|
}
|
||||||
|
@ -91,7 +92,7 @@ ALWAYS_INLINE_RELEASE static void ResizeColumnsForView(T* view, const std::initi
|
||||||
continue;
|
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);
|
view->setColumnWidth(column_index, width);
|
||||||
column_index++;
|
column_index++;
|
||||||
}
|
}
|
||||||
|
@ -772,4 +773,4 @@ std::optional<unsigned> PromptForAddress(QWidget* parent, const QString& title,
|
||||||
return address;
|
return address;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace QtUtils
|
} // namespace QtUtils
|
||||||
|
|
|
@ -93,10 +93,8 @@ void SettingsDialog::setCategoryHelpTexts()
|
||||||
"console.<br><br>Mouse over an option for additional information.");
|
"console.<br><br>Mouse over an option for additional information.");
|
||||||
m_category_help_text[static_cast<int>(Category::GameListSettings)] =
|
m_category_help_text[static_cast<int>(Category::GameListSettings)] =
|
||||||
tr("<strong>Game List Settings</strong><hr>The list above shows the directories which will be searched by "
|
tr("<strong>Game List Settings</strong><hr>The list above shows the directories which will be searched by "
|
||||||
"DuckStation "
|
"DuckStation to populate the game list. Search directories can be added, removed, and switched to "
|
||||||
"to populate the game list. Search directories can be added, removed, and switched to recursive/non-recursive. "
|
"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.");
|
|
||||||
m_category_help_text[static_cast<int>(Category::HotkeySettings)] = tr(
|
m_category_help_text[static_cast<int>(Category::HotkeySettings)] = tr(
|
||||||
"<strong>Hotkey Settings</strong><hr>Binding a hotkey allows you to trigger events such as a resetting or taking "
|
"<strong>Hotkey Settings</strong><hr>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 "
|
"screenshots at the press of a key/controller button. Hotkey titles are self-explanatory. Clicking a binding will "
|
||||||
|
|
|
@ -9,6 +9,8 @@ add_library(frontend-common
|
||||||
fullscreen_ui.h
|
fullscreen_ui.h
|
||||||
fullscreen_ui_progress_callback.cpp
|
fullscreen_ui_progress_callback.cpp
|
||||||
fullscreen_ui_progress_callback.h
|
fullscreen_ui_progress_callback.h
|
||||||
|
game_database.cpp
|
||||||
|
game_database.h
|
||||||
game_list.cpp
|
game_list.cpp
|
||||||
game_list.h
|
game_list.h
|
||||||
game_settings.cpp
|
game_settings.cpp
|
||||||
|
|
|
@ -85,7 +85,6 @@ bool CommonHostInterface::Initialize()
|
||||||
|
|
||||||
m_game_list = std::make_unique<GameList>();
|
m_game_list = std::make_unique<GameList>();
|
||||||
m_game_list->SetCacheFilename(GetUserDirectoryRelativePath("cache/gamelist.cache"));
|
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->SetUserCompatibilityListFilename(GetUserDirectoryRelativePath("compatibility.xml"));
|
||||||
m_game_list->SetUserGameSettingsFilename(GetUserDirectoryRelativePath("gamesettings.ini"));
|
m_game_list->SetUserGameSettingsFilename(GetUserDirectoryRelativePath("gamesettings.ini"));
|
||||||
|
|
||||||
|
@ -2865,9 +2864,10 @@ void CommonHostInterface::GetGameInfo(const char* path, CDImage* image, std::str
|
||||||
if (image)
|
if (image)
|
||||||
*code = System::GetGameCodeForImage(image, true);
|
*code = System::GetGameCodeForImage(image, true);
|
||||||
|
|
||||||
const GameListDatabaseEntry* db_entry = (!code->empty()) ? m_game_list->GetDatabaseEntryForCode(*code) : nullptr;
|
GameDatabase database;
|
||||||
if (db_entry)
|
GameDatabaseEntry database_entry;
|
||||||
*title = db_entry->title;
|
if (database.Load() && database.GetEntryForDisc(image, &database_entry))
|
||||||
|
*title = std::move(database_entry.title);
|
||||||
else
|
else
|
||||||
*title = System::GetTitleForPath(path);
|
*title = System::GetTitleForPath(path);
|
||||||
}
|
}
|
||||||
|
@ -2978,15 +2978,73 @@ bool CommonHostInterface::SaveScreenshot(const char* filename /* = nullptr */, b
|
||||||
|
|
||||||
void CommonHostInterface::ApplyGameSettings(bool display_osd_messages)
|
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
|
// this gets called while booting, so can't use valid
|
||||||
if (System::IsShutdown() || System::GetRunningCode().empty() || !g_settings.apply_game_settings)
|
if (System::IsShutdown() || System::GetRunningCode().empty() || !g_settings.apply_game_settings)
|
||||||
return;
|
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());
|
const GameSettings::Entry* gs = m_game_list->GetGameSettings(System::GetRunningPath(), System::GetRunningCode());
|
||||||
if (gs)
|
if (gs)
|
||||||
gs->ApplySettings(display_osd_messages);
|
gs->ApplySettings(display_osd_messages);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CommonHostInterface::ApplyControllerCompatibilitySettings(u64 controller_mask, bool display_osd_messages)
|
||||||
|
{
|
||||||
|
#define BIT_FOR(ctype) (static_cast<u64>(1) << static_cast<u32>(ctype))
|
||||||
|
|
||||||
|
if (controller_mask == 0 || controller_mask == static_cast<u64>(-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<u32>(ControllerType::Count); j++)
|
||||||
|
{
|
||||||
|
const ControllerType supported_ctype = static_cast<ControllerType>(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()
|
bool CommonHostInterface::UpdateControllerInputMapFromGameSettings()
|
||||||
{
|
{
|
||||||
// this gets called while booting, so can't use valid
|
// this gets called while booting, so can't use valid
|
||||||
|
|
|
@ -15,6 +15,9 @@
|
||||||
|
|
||||||
class HostDisplayTexture;
|
class HostDisplayTexture;
|
||||||
|
|
||||||
|
class GameList;
|
||||||
|
struct GameDatabaseEntry;
|
||||||
|
|
||||||
class ControllerInterface;
|
class ControllerInterface;
|
||||||
|
|
||||||
namespace FrontendCommon {
|
namespace FrontendCommon {
|
||||||
|
@ -411,6 +414,7 @@ protected:
|
||||||
void RecreateSystem() override;
|
void RecreateSystem() override;
|
||||||
|
|
||||||
void ApplyGameSettings(bool display_osd_messages);
|
void ApplyGameSettings(bool display_osd_messages);
|
||||||
|
void ApplyControllerCompatibilitySettings(u64 controller_mask, bool display_osd_messages);
|
||||||
|
|
||||||
bool CreateHostDisplayResources();
|
bool CreateHostDisplayResources();
|
||||||
void ReleaseHostDisplayResources();
|
void ReleaseHostDisplayResources();
|
||||||
|
|
|
@ -93,6 +93,7 @@
|
||||||
<ClCompile Include="dinput_controller_interface.cpp" />
|
<ClCompile Include="dinput_controller_interface.cpp" />
|
||||||
<ClCompile Include="fullscreen_ui.cpp" />
|
<ClCompile Include="fullscreen_ui.cpp" />
|
||||||
<ClCompile Include="fullscreen_ui_progress_callback.cpp" />
|
<ClCompile Include="fullscreen_ui_progress_callback.cpp" />
|
||||||
|
<ClCompile Include="game_database.cpp" />
|
||||||
<ClCompile Include="game_list.cpp" />
|
<ClCompile Include="game_list.cpp" />
|
||||||
<ClCompile Include="game_settings.cpp" />
|
<ClCompile Include="game_settings.cpp" />
|
||||||
<ClCompile Include="http_downloader.cpp" />
|
<ClCompile Include="http_downloader.cpp" />
|
||||||
|
@ -125,6 +126,7 @@
|
||||||
<ClInclude Include="dinput_controller_interface.h" />
|
<ClInclude Include="dinput_controller_interface.h" />
|
||||||
<ClInclude Include="fullscreen_ui.h" />
|
<ClInclude Include="fullscreen_ui.h" />
|
||||||
<ClInclude Include="fullscreen_ui_progress_callback.h" />
|
<ClInclude Include="fullscreen_ui_progress_callback.h" />
|
||||||
|
<ClInclude Include="game_database.h" />
|
||||||
<ClInclude Include="game_list.h" />
|
<ClInclude Include="game_list.h" />
|
||||||
<ClInclude Include="game_settings.h" />
|
<ClInclude Include="game_settings.h" />
|
||||||
<ClInclude Include="http_downloader.h" />
|
<ClInclude Include="http_downloader.h" />
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
<ClCompile Include="http_downloader.cpp" />
|
<ClCompile Include="http_downloader.cpp" />
|
||||||
<ClCompile Include="http_downloader_winhttp.cpp" />
|
<ClCompile Include="http_downloader_winhttp.cpp" />
|
||||||
<ClCompile Include="input_overlay_ui.cpp" />
|
<ClCompile Include="input_overlay_ui.cpp" />
|
||||||
|
<ClCompile Include="game_database.cpp" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ClInclude Include="icon.h" />
|
<ClInclude Include="icon.h" />
|
||||||
|
@ -63,6 +64,7 @@
|
||||||
<ClInclude Include="http_downloader.h" />
|
<ClInclude Include="http_downloader.h" />
|
||||||
<ClInclude Include="http_downloader_winhttp.h" />
|
<ClInclude Include="http_downloader_winhttp.h" />
|
||||||
<ClInclude Include="input_overlay_ui.h" />
|
<ClInclude Include="input_overlay_ui.h" />
|
||||||
|
<ClInclude Include="game_database.h" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="font_roboto_regular.inl" />
|
<None Include="font_roboto_regular.inl" />
|
||||||
|
|
|
@ -2871,6 +2871,14 @@ void DrawGameListWindow()
|
||||||
|
|
||||||
ImGui::PushFont(g_medium_font);
|
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
|
// code
|
||||||
text_width = ImGui::CalcTextSize(selected_entry->code.c_str(), nullptr, false, work_width).x;
|
text_width = ImGui::CalcTextSize(selected_entry->code.c_str(), nullptr, false, work_width).x;
|
||||||
ImGui::SetCursorPosX((work_width - text_width) / 2.0f);
|
ImGui::SetCursorPosX((work_width - text_width) / 2.0f);
|
||||||
|
@ -2885,6 +2893,14 @@ void DrawGameListWindow()
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::Text(" (%s)", Settings::GetDiscRegionDisplayName(selected_entry->region));
|
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
|
// compatibility
|
||||||
ImGui::TextUnformatted("Compatibility: ");
|
ImGui::TextUnformatted("Compatibility: ");
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
|
@ -2896,9 +2912,6 @@ void DrawGameListWindow()
|
||||||
// size
|
// size
|
||||||
ImGui::Text("Size: %.2f MB", static_cast<float>(selected_entry->total_size) / 1048576.0f);
|
ImGui::Text("Size: %.2f MB", static_cast<float>(selected_entry->total_size) / 1048576.0f);
|
||||||
|
|
||||||
// TODO: last played
|
|
||||||
ImGui::Text("Last Played: Never");
|
|
||||||
|
|
||||||
// game settings
|
// game settings
|
||||||
const u32 user_setting_count = selected_entry->settings.GetUserSettingsCount();
|
const u32 user_setting_count = selected_entry->settings.GetUserSettingsCount();
|
||||||
if (user_setting_count > 0)
|
if (user_setting_count > 0)
|
||||||
|
|
225
src/frontend-common/game_database.cpp
Normal file
225
src/frontend-common/game_database.cpp
Normal file
|
@ -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 <iomanip>
|
||||||
|
Log_SetChannel(GameDatabase);
|
||||||
|
|
||||||
|
GameDatabase::GameDatabase() = default;
|
||||||
|
|
||||||
|
GameDatabase::~GameDatabase()
|
||||||
|
{
|
||||||
|
Unload();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GameDatabase::Load()
|
||||||
|
{
|
||||||
|
// TODO: use stream directly
|
||||||
|
std::unique_ptr<ByteStream> 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<rapidjson::Document> json = std::make_unique<rapidjson::Document>();
|
||||||
|
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<rapidjson::Document*>(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<rapidjson::Document*>(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<ControllerType> 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<u32>(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;
|
||||||
|
}
|
45
src/frontend-common/game_database.h
Normal file
45
src/frontend-common/game_database.h
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
#pragma once
|
||||||
|
#include "core/types.h"
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
|
@ -16,6 +16,7 @@
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
|
#include <ctime>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <tinyxml2.h>
|
#include <tinyxml2.h>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
@ -158,40 +159,50 @@ bool GameList::GetGameListEntry(const std::string& path, GameListEntry* entry)
|
||||||
if (!cdi)
|
if (!cdi)
|
||||||
return false;
|
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->path = path;
|
||||||
entry->code = std::move(code);
|
|
||||||
entry->region = region;
|
|
||||||
entry->total_size = static_cast<u64>(CDImage::RAW_SECTOR_SIZE) * static_cast<u64>(cdi->GetLBACount());
|
entry->total_size = static_cast<u64>(CDImage::RAW_SECTOR_SIZE) * static_cast<u64>(cdi->GetLBACount());
|
||||||
entry->type = GameListEntryType::Disc;
|
entry->type = GameListEntryType::Disc;
|
||||||
entry->compatibility_rating = GameListCompatibilityRating::Unknown;
|
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
|
// no game code, so use the filename title
|
||||||
|
entry->code = System::GetGameCodeForImage(cdi.get(), true);
|
||||||
entry->title = System::GetTitleForPath(path.c_str());
|
entry->title = System::GetTitleForPath(path.c_str());
|
||||||
entry->compatibility_rating = GameListCompatibilityRating::Unknown;
|
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
|
else
|
||||||
{
|
{
|
||||||
const GameListDatabaseEntry* database_entry = GetDatabaseEntryForCode(entry->code);
|
// pull from database
|
||||||
if (database_entry)
|
entry->code = std::move(dbentry.serial);
|
||||||
{
|
entry->title = std::move(dbentry.title);
|
||||||
entry->title = database_entry->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)
|
// region detection
|
||||||
Log_WarningPrintf("Region mismatch between disc and database for '%s'", entry->code.c_str());
|
entry->region = System::GetRegionFromSystemArea(cdi.get());
|
||||||
}
|
if (entry->region == DiscRegion::Other)
|
||||||
else
|
entry->region = System::GetRegionForCode(entry->code);
|
||||||
{
|
|
||||||
Log_WarningPrintf("'%s' not found in database", entry->code.c_str());
|
|
||||||
entry->title = System::GetTitleForPath(path.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (!entry->code.empty())
|
||||||
|
{
|
||||||
const GameListCompatibilityEntry* compatibility_entry = GetCompatibilityEntryForCode(entry->code);
|
const GameListCompatibilityEntry* compatibility_entry = GetCompatibilityEntryForCode(entry->code);
|
||||||
if (compatibility_entry)
|
if (compatibility_entry)
|
||||||
entry->compatibility_rating = compatibility_entry->compatibility_rating;
|
entry->compatibility_rating = compatibility_entry->compatibility_rating;
|
||||||
|
@ -328,30 +339,27 @@ bool GameList::LoadEntriesFromCache(ByteStream* stream)
|
||||||
while (stream->GetPosition() != stream->GetSize())
|
while (stream->GetPosition() != stream->GetSize())
|
||||||
{
|
{
|
||||||
std::string path;
|
std::string path;
|
||||||
std::string code;
|
GameListEntry ge;
|
||||||
std::string title;
|
|
||||||
u64 total_size;
|
|
||||||
u64 last_modified_time;
|
|
||||||
u8 region;
|
|
||||||
u8 type;
|
u8 type;
|
||||||
|
u8 region;
|
||||||
u8 compatibility_rating;
|
u8 compatibility_rating;
|
||||||
|
|
||||||
if (!ReadString(stream, &path) || !ReadString(stream, &code) || !ReadString(stream, &title) ||
|
if (!ReadU8(stream, &type) || !ReadU8(stream, ®ion) || !ReadString(stream, &path) ||
|
||||||
!ReadU64(stream, &total_size) || !ReadU64(stream, &last_modified_time) || !ReadU8(stream, ®ion) ||
|
!ReadString(stream, &ge.code) || !ReadString(stream, &ge.title) || !ReadString(stream, &ge.genre) ||
|
||||||
region >= static_cast<u8>(DiscRegion::Count) || !ReadU8(stream, &type) ||
|
!ReadString(stream, &ge.publisher) || !ReadString(stream, &ge.developer) || !ReadU64(stream, &ge.total_size) ||
|
||||||
type >= static_cast<u8>(GameListEntryType::Count) || !ReadU8(stream, &compatibility_rating) ||
|
!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<u8>(DiscRegion::Count) ||
|
||||||
|
type >= static_cast<u8>(GameListEntryType::Count) ||
|
||||||
compatibility_rating >= static_cast<u8>(GameListCompatibilityRating::Count))
|
compatibility_rating >= static_cast<u8>(GameListCompatibilityRating::Count))
|
||||||
{
|
{
|
||||||
Log_WarningPrintf("Game list cache entry is corrupted");
|
Log_WarningPrintf("Game list cache entry is corrupted");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
GameListEntry ge;
|
|
||||||
ge.path = path;
|
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<DiscRegion>(region);
|
ge.region = static_cast<DiscRegion>(region);
|
||||||
ge.type = static_cast<GameListEntryType>(type);
|
ge.type = static_cast<GameListEntryType>(type);
|
||||||
ge.compatibility_rating = static_cast<GameListCompatibilityRating>(compatibility_rating);
|
ge.compatibility_rating = static_cast<GameListCompatibilityRating>(compatibility_rating);
|
||||||
|
@ -405,13 +413,23 @@ bool GameList::OpenCacheForWriting()
|
||||||
|
|
||||||
bool GameList::WriteEntryToCache(const GameListEntry* entry, ByteStream* stream)
|
bool GameList::WriteEntryToCache(const GameListEntry* entry, ByteStream* stream)
|
||||||
{
|
{
|
||||||
bool result = WriteString(stream, entry->path);
|
bool result = true;
|
||||||
|
result &= WriteU8(stream, static_cast<u8>(entry->type));
|
||||||
|
result &= WriteU8(stream, static_cast<u8>(entry->region));
|
||||||
|
result &= WriteString(stream, entry->path);
|
||||||
result &= WriteString(stream, entry->code);
|
result &= WriteString(stream, entry->code);
|
||||||
result &= WriteString(stream, entry->title);
|
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->total_size);
|
||||||
result &= WriteU64(stream, entry->last_modified_time);
|
result &= WriteU64(stream, entry->last_modified_time);
|
||||||
result &= WriteU8(stream, static_cast<u8>(entry->region));
|
result &= WriteU64(stream, entry->release_date);
|
||||||
result &= WriteU8(stream, static_cast<u8>(entry->type));
|
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<u8>(entry->compatibility_rating));
|
result &= WriteU8(stream, static_cast<u8>(entry->compatibility_rating));
|
||||||
result &= entry->settings.SaveToStream(stream);
|
result &= entry->settings.SaveToStream(stream);
|
||||||
return result;
|
return result;
|
||||||
|
@ -524,84 +542,6 @@ void GameList::ScanDirectory(const char* path, bool recursive, ProgressCallback*
|
||||||
progress->PopState();
|
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<char>(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 <serial>SCES-00984, SCES-00984#</serial>
|
|
||||||
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)
|
void GameList::AddDirectory(std::string path, bool recursive)
|
||||||
{
|
{
|
||||||
auto iter = std::find_if(m_search_directories.begin(), m_search_directories.end(),
|
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;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GameListDatabaseEntry* GameList::GetDatabaseEntryForCode(const std::string& code) const
|
|
||||||
{
|
|
||||||
if (!m_database_load_tried)
|
|
||||||
const_cast<GameList*>(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
|
const GameListCompatibilityEntry* GameList::GetCompatibilityEntryForCode(const std::string& code) const
|
||||||
{
|
{
|
||||||
if (!m_compatibility_list_load_tried)
|
if (!m_compatibility_list_load_tried)
|
||||||
|
@ -734,54 +665,12 @@ void GameList::LoadDatabase()
|
||||||
return;
|
return;
|
||||||
|
|
||||||
m_database_load_tried = true;
|
m_database_load_tried = true;
|
||||||
|
m_database.Load();
|
||||||
tinyxml2::XMLDocument doc;
|
|
||||||
if (FileSystem::FileExists(m_user_database_filename.c_str()))
|
|
||||||
{
|
|
||||||
std::unique_ptr<ByteStream> 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<ByteStream> 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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameList::ClearDatabase()
|
void GameList::ClearDatabase()
|
||||||
{
|
{
|
||||||
m_database.clear();
|
m_database.Unload();
|
||||||
m_database_load_tried = false;
|
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(),
|
return g_host_interface->GetUserDirectoryRelativePath("covers" FS_OSPATH_SEPARATOR_STR "%s%s", entry->title.c_str(),
|
||||||
extension);
|
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<std::time_t>(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);
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "core/types.h"
|
#include "core/types.h"
|
||||||
#include "game_settings.h"
|
#include "game_settings.h"
|
||||||
|
#include "game_database.h"
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
@ -34,24 +35,31 @@ enum class GameListCompatibilityRating
|
||||||
Count,
|
Count,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct GameListDatabaseEntry
|
|
||||||
{
|
|
||||||
std::string code;
|
|
||||||
std::string title;
|
|
||||||
DiscRegion region;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct GameListEntry
|
struct GameListEntry
|
||||||
{
|
{
|
||||||
|
GameListEntryType type;
|
||||||
|
DiscRegion region;
|
||||||
|
|
||||||
std::string path;
|
std::string path;
|
||||||
std::string code;
|
std::string code;
|
||||||
std::string title;
|
std::string title;
|
||||||
|
std::string genre;
|
||||||
|
std::string publisher;
|
||||||
|
std::string developer;
|
||||||
u64 total_size;
|
u64 total_size;
|
||||||
u64 last_modified_time;
|
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;
|
GameListCompatibilityRating compatibility_rating;
|
||||||
GameSettings::Entry settings;
|
GameSettings::Entry settings;
|
||||||
|
|
||||||
|
size_t GetReleaseDateString(char* buffer, size_t buffer_size) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct GameListCompatibilityEntry
|
struct GameListCompatibilityEntry
|
||||||
|
@ -93,11 +101,11 @@ public:
|
||||||
const u32 GetSearchDirectoryCount() const { return static_cast<u32>(m_search_directories.size()); }
|
const u32 GetSearchDirectoryCount() const { return static_cast<u32>(m_search_directories.size()); }
|
||||||
|
|
||||||
const GameListEntry* GetEntryForPath(const char* path) const;
|
const GameListEntry* GetEntryForPath(const char* path) const;
|
||||||
const GameListDatabaseEntry* GetDatabaseEntryForCode(const std::string& code) const;
|
|
||||||
const GameListCompatibilityEntry* GetCompatibilityEntryForCode(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 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)
|
void SetUserCompatibilityListFilename(std::string filename)
|
||||||
{
|
{
|
||||||
m_user_compatibility_list_filename = std::move(filename);
|
m_user_compatibility_list_filename = std::move(filename);
|
||||||
|
@ -123,10 +131,9 @@ private:
|
||||||
enum : u32
|
enum : u32
|
||||||
{
|
{
|
||||||
GAME_LIST_CACHE_SIGNATURE = 0x45434C47,
|
GAME_LIST_CACHE_SIGNATURE = 0x45434C47,
|
||||||
GAME_LIST_CACHE_VERSION = 25
|
GAME_LIST_CACHE_VERSION = 26
|
||||||
};
|
};
|
||||||
|
|
||||||
using DatabaseMap = std::unordered_map<std::string, GameListDatabaseEntry>;
|
|
||||||
using CacheMap = std::unordered_map<std::string, GameListEntry>;
|
using CacheMap = std::unordered_map<std::string, GameListEntry>;
|
||||||
using CompatibilityMap = std::unordered_map<std::string, GameListCompatibilityEntry>;
|
using CompatibilityMap = std::unordered_map<std::string, GameListCompatibilityEntry>;
|
||||||
|
|
||||||
|
@ -160,16 +167,15 @@ private:
|
||||||
|
|
||||||
void LoadGameSettings();
|
void LoadGameSettings();
|
||||||
|
|
||||||
DatabaseMap m_database;
|
|
||||||
EntryList m_entries;
|
EntryList m_entries;
|
||||||
CacheMap m_cache_map;
|
CacheMap m_cache_map;
|
||||||
|
GameDatabase m_database;
|
||||||
CompatibilityMap m_compatibility_list;
|
CompatibilityMap m_compatibility_list;
|
||||||
GameSettings::Database m_game_settings;
|
GameSettings::Database m_game_settings;
|
||||||
std::unique_ptr<ByteStream> m_cache_write_stream;
|
std::unique_ptr<ByteStream> m_cache_write_stream;
|
||||||
|
|
||||||
std::vector<DirectoryEntry> m_search_directories;
|
std::vector<DirectoryEntry> m_search_directories;
|
||||||
std::string m_cache_filename;
|
std::string m_cache_filename;
|
||||||
std::string m_user_database_filename;
|
|
||||||
std::string m_user_compatibility_list_filename;
|
std::string m_user_compatibility_list_filename;
|
||||||
std::string m_user_game_settings_filename;
|
std::string m_user_game_settings_filename;
|
||||||
bool m_database_load_tried = false;
|
bool m_database_load_tried = false;
|
||||||
|
|
|
@ -33,7 +33,6 @@ std::array<std::pair<const char*, const char*>, static_cast<u32>(Trait::Count)>
|
||||||
{"DisablePGXPDepthBuffer", TRANSLATABLE("GameSettingsTrait", "Disable PGXP Depth Buffer")},
|
{"DisablePGXPDepthBuffer", TRANSLATABLE("GameSettingsTrait", "Disable PGXP Depth Buffer")},
|
||||||
{"ForcePGXPVertexCache", TRANSLATABLE("GameSettingsTrait", "Force PGXP Vertex Cache")},
|
{"ForcePGXPVertexCache", TRANSLATABLE("GameSettingsTrait", "Force PGXP Vertex Cache")},
|
||||||
{"ForcePGXPCPUMode", TRANSLATABLE("GameSettingsTrait", "Force PGXP CPU Mode")},
|
{"ForcePGXPCPUMode", TRANSLATABLE("GameSettingsTrait", "Force PGXP CPU Mode")},
|
||||||
{"DisableAnalogModeForcing", TRANSLATABLE("GameSettingsTrait", "Disable Forcing Controller Analog Mode on Reset")},
|
|
||||||
{"ForceRecompilerMemoryExceptions", TRANSLATABLE("GameSettingsTrait", "Force Recompiler Memory Exceptions")},
|
{"ForceRecompilerMemoryExceptions", TRANSLATABLE("GameSettingsTrait", "Force Recompiler Memory Exceptions")},
|
||||||
{"ForceRecompilerICache", TRANSLATABLE("GameSettingsTrait", "Force Recompiler ICache")},
|
{"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;
|
g_settings.gpu_pgxp_depth_buffer = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (HasTrait(Trait::DisableAnalogModeForcing))
|
|
||||||
g_settings.controller_disable_analog_mode_forcing = true;
|
|
||||||
|
|
||||||
if (HasTrait(Trait::ForceRecompilerMemoryExceptions))
|
if (HasTrait(Trait::ForceRecompilerMemoryExceptions))
|
||||||
g_settings.cpu_recompiler_memory_exceptions = true;
|
g_settings.cpu_recompiler_memory_exceptions = true;
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,6 @@ enum class Trait : u32
|
||||||
DisablePGXPDepthBuffer,
|
DisablePGXPDepthBuffer,
|
||||||
ForcePGXPVertexCache,
|
ForcePGXPVertexCache,
|
||||||
ForcePGXPCPUMode,
|
ForcePGXPCPUMode,
|
||||||
DisableAnalogModeForcing,
|
|
||||||
ForceRecompilerMemoryExceptions,
|
ForceRecompilerMemoryExceptions,
|
||||||
ForceRecompilerICache,
|
ForceRecompilerICache,
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue