mirror of
https://github.com/WinampDesktop/winamp.git
synced 2025-06-18 19:55:46 -04:00
Add JSON game database to replace dat parsing
This commit is contained in:
@ -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);
|
||||
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));
|
||||
|
||||
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))
|
||||
num_flex_items++;
|
||||
if (!view->isColumnHidden(column_index))
|
||||
{
|
||||
if (spec_width < 0)
|
||||
num_flex_items++;
|
||||
else
|
||||
total_width += std::max(spec_width, min_column_width);
|
||||
}
|
||||
|
||||
column_index++;
|
||||
}
|
||||
@ -91,7 +92,7 @@ ALWAYS_INLINE_RELEASE static void ResizeColumnsForView(T* view, const std::initi
|
||||
continue;
|
||||
}
|
||||
|
||||
const int width = spec_width < 0 ? flex_width : spec_width;
|
||||
const int width = spec_width < 0 ? flex_width : (std::max(spec_width, min_column_width));
|
||||
view->setColumnWidth(column_index, width);
|
||||
column_index++;
|
||||
}
|
||||
@ -772,4 +773,4 @@ std::optional<unsigned> PromptForAddress(QWidget* parent, const QString& title,
|
||||
return address;
|
||||
}
|
||||
|
||||
} // namespace QtUtils
|
||||
} // namespace QtUtils
|
||||
|
@ -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 "
|
||||
|
Reference in New Issue
Block a user