mirror of
https://github.com/RetroDECK/Duckstation.git
synced 2024-11-25 07:05:40 +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)
|
||||
return {};
|
||||
|
||||
return GetGameHashCodeForImage(cdi);
|
||||
}
|
||||
|
||||
std::string GetGameHashCodeForImage(CDImage* cdi)
|
||||
{
|
||||
std::string exe_name;
|
||||
std::vector<u8> exe_buffer;
|
||||
if (!ReadExecutableFromImage(cdi, &exe_name, &exe_buffer))
|
||||
|
|
|
@ -72,6 +72,7 @@ ConsoleRegion GetConsoleRegionForDiscRegion(DiscRegion region);
|
|||
std::string GetExecutableNameForImage(CDImage* cdi);
|
||||
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 GetGameCodeForPath(const char* image_path, bool fallback_to_hash);
|
||||
DiscRegion GetRegionForCode(std::string_view code);
|
||||
|
|
|
@ -2,11 +2,14 @@
|
|||
#include "common/file_system.h"
|
||||
#include "common/string_util.h"
|
||||
#include "core/system.h"
|
||||
#include <QtCore/QDate>
|
||||
#include <QtCore/QDateTime>
|
||||
#include <QtGui/QIcon>
|
||||
#include <QtGui/QPainter>
|
||||
|
||||
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_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()));
|
||||
}
|
||||
|
||||
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:
|
||||
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()));
|
||||
}
|
||||
|
||||
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:
|
||||
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_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");
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 <QtCore/QDebug>
|
||||
#include <QtCore/QSettings>
|
||||
#include <QtCore/QUrl>
|
||||
#include <QtNetwork/QNetworkAccessManager>
|
||||
#include <QtNetwork/QNetworkReply>
|
||||
#include <QtNetwork/QNetworkRequest>
|
||||
#include <QtWidgets/QFileDialog>
|
||||
#include <QtWidgets/QHeaderView>
|
||||
#include <QtWidgets/QMenu>
|
||||
#include <QtWidgets/QMessageBox>
|
||||
#include <QtWidgets/QProgressDialog>
|
||||
#include <algorithm>
|
||||
|
||||
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<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 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;
|
||||
|
|
|
@ -121,23 +121,6 @@
|
|||
</property>
|
||||
</widget>
|
||||
</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>
|
||||
</item>
|
||||
</layout>
|
||||
|
|
|
@ -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<bool, GameListModel::Column_Count> DEFAULT_VISIBILITY = {
|
||||
{true, true, true, false, true, true, true}};
|
||||
static constexpr std::array<bool, GameListModel::Column_Count> 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++)
|
||||
{
|
||||
|
|
|
@ -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<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));
|
||||
m_ui.imagePath->setText(QString::fromStdString(ge->path));
|
||||
m_ui.title->setText(title_qstring);
|
||||
|
||||
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));
|
||||
|
||||
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;
|
||||
|
|
|
@ -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))
|
||||
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++;
|
||||
}
|
||||
|
|
|
@ -93,10 +93,8 @@ void SettingsDialog::setCategoryHelpTexts()
|
|||
"console.<br><br>Mouse over an option for additional information.");
|
||||
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 "
|
||||
"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<int>(Category::HotkeySettings)] = tr(
|
||||
"<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 "
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -85,7 +85,6 @@ bool CommonHostInterface::Initialize()
|
|||
|
||||
m_game_list = std::make_unique<GameList>();
|
||||
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<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()
|
||||
{
|
||||
// this gets called while booting, so can't use valid
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -93,6 +93,7 @@
|
|||
<ClCompile Include="dinput_controller_interface.cpp" />
|
||||
<ClCompile Include="fullscreen_ui.cpp" />
|
||||
<ClCompile Include="fullscreen_ui_progress_callback.cpp" />
|
||||
<ClCompile Include="game_database.cpp" />
|
||||
<ClCompile Include="game_list.cpp" />
|
||||
<ClCompile Include="game_settings.cpp" />
|
||||
<ClCompile Include="http_downloader.cpp" />
|
||||
|
@ -125,6 +126,7 @@
|
|||
<ClInclude Include="dinput_controller_interface.h" />
|
||||
<ClInclude Include="fullscreen_ui.h" />
|
||||
<ClInclude Include="fullscreen_ui_progress_callback.h" />
|
||||
<ClInclude Include="game_database.h" />
|
||||
<ClInclude Include="game_list.h" />
|
||||
<ClInclude Include="game_settings.h" />
|
||||
<ClInclude Include="http_downloader.h" />
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
<ClCompile Include="http_downloader.cpp" />
|
||||
<ClCompile Include="http_downloader_winhttp.cpp" />
|
||||
<ClCompile Include="input_overlay_ui.cpp" />
|
||||
<ClCompile Include="game_database.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="icon.h" />
|
||||
|
@ -63,6 +64,7 @@
|
|||
<ClInclude Include="http_downloader.h" />
|
||||
<ClInclude Include="http_downloader_winhttp.h" />
|
||||
<ClInclude Include="input_overlay_ui.h" />
|
||||
<ClInclude Include="game_database.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="font_roboto_regular.inl" />
|
||||
|
|
|
@ -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<float>(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)
|
||||
|
|
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 <array>
|
||||
#include <cctype>
|
||||
#include <ctime>
|
||||
#include <string_view>
|
||||
#include <tinyxml2.h>
|
||||
#include <utility>
|
||||
|
@ -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<u64>(CDImage::RAW_SECTOR_SIZE) * static_cast<u64>(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;
|
||||
|
||||
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());
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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<u8>(DiscRegion::Count) || !ReadU8(stream, &type) ||
|
||||
type >= static_cast<u8>(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<u8>(DiscRegion::Count) ||
|
||||
type >= static_cast<u8>(GameListEntryType::Count) ||
|
||||
compatibility_rating >= static_cast<u8>(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<DiscRegion>(region);
|
||||
ge.type = static_cast<GameListEntryType>(type);
|
||||
ge.compatibility_rating = static_cast<GameListCompatibilityRating>(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<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->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<u8>(entry->region));
|
||||
result &= WriteU8(stream, static_cast<u8>(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<u8>(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<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)
|
||||
{
|
||||
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<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
|
||||
{
|
||||
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<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());
|
||||
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<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
|
||||
#include "core/types.h"
|
||||
#include "game_settings.h"
|
||||
#include "game_database.h"
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
@ -34,24 +35,31 @@ enum class GameListCompatibilityRating
|
|||
Count,
|
||||
};
|
||||
|
||||
struct GameListDatabaseEntry
|
||||
{
|
||||
std::string code;
|
||||
std::string title;
|
||||
DiscRegion region;
|
||||
};
|
||||
|
||||
struct GameListEntry
|
||||
{
|
||||
GameListEntryType type;
|
||||
DiscRegion region;
|
||||
|
||||
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<u32>(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<std::string, GameListDatabaseEntry>;
|
||||
using CacheMap = std::unordered_map<std::string, GameListEntry>;
|
||||
using CompatibilityMap = std::unordered_map<std::string, GameListCompatibilityEntry>;
|
||||
|
||||
|
@ -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<ByteStream> m_cache_write_stream;
|
||||
|
||||
std::vector<DirectoryEntry> 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;
|
||||
|
|
|
@ -33,7 +33,6 @@ std::array<std::pair<const char*, const char*>, static_cast<u32>(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;
|
||||
|
||||
|
|
|
@ -25,7 +25,6 @@ enum class Trait : u32
|
|||
DisablePGXPDepthBuffer,
|
||||
ForcePGXPVertexCache,
|
||||
ForcePGXPCPUMode,
|
||||
DisableAnalogModeForcing,
|
||||
ForceRecompilerMemoryExceptions,
|
||||
ForceRecompilerICache,
|
||||
|
||||
|
|
Loading…
Reference in a new issue