Add JSON game database to replace dat parsing

This commit is contained in:
Connor McLaughlin 2021-04-17 14:23:47 +10:00
parent b25030b19a
commit ff14e8aede
24 changed files with 595 additions and 398 deletions

20
scripts/merge_gamedb.py Normal file
View 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])

View file

@ -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))

View file

@ -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);

View file

@ -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");

View file

@ -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,

View file

@ -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);
}

View file

@ -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;

View file

@ -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>

View file

@ -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++)
{

View file

@ -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;

View file

@ -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++;
}

View file

@ -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 "

View file

@ -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

View file

@ -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

View file

@ -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();

View file

@ -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" />

View file

@ -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" />

View file

@ -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)

View 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 &current;
}
}
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;
}

View 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;
};

View file

@ -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, &region) ||
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, &region) || !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);
}

View file

@ -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;

View file

@ -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;

View file

@ -25,7 +25,6 @@ enum class Trait : u32
DisablePGXPDepthBuffer,
ForcePGXPVertexCache,
ForcePGXPCPUMode,
DisableAnalogModeForcing,
ForceRecompilerMemoryExceptions,
ForceRecompilerICache,