Qt: Add disc path option for ELF game list entries

This commit is contained in:
Connor McLaughlin 2022-12-04 18:03:30 +10:00 committed by refractionpcsx2
parent 8ac0bd452e
commit 9da8e9280f
20 changed files with 687 additions and 361 deletions

View File

@ -35,6 +35,8 @@
#include "QtHost.h"
#include "QtUtils.h"
#include "fmt/format.h"
static const char* SUPPORTED_FORMATS_STRING = QT_TRANSLATE_NOOP(GameListWidget,
".bin/.iso (ISO Disc Images)\n"
".chd (Compressed Hunks of Data)\n"
@ -596,6 +598,19 @@ const GameList::Entry* GameListWidget::getSelectedEntry() const
}
}
void GameListWidget::rescanFile(const std::string& path)
{
// We can't do this while there's a VM running, because of CDVD state... ugh.
if (QtHost::IsVMValid())
{
Console.Error(fmt::format("Can't re-scan ELF at '{}' because we have a VM running.", path));
return;
}
GameList::RescanPath(path);
m_model->refresh();
}
GameListGridListView::GameListGridListView(QWidget* parent /*= nullptr*/)
: QListView(parent)
{

View File

@ -64,6 +64,9 @@ public:
const GameList::Entry* getSelectedEntry() const;
/// Rescans a single file. NOTE: Happens on UI thread.
void rescanFile(const std::string& path);
Q_SIGNALS:
void refreshProgress(const QString& status, int current, int total);
void refreshComplete();

View File

@ -66,7 +66,7 @@
#endif
static constexpr char OPEN_FILE_FILTER[] =
const char* MainWindow::OPEN_FILE_FILTER =
QT_TRANSLATE_NOOP("MainWindow", "All File Types (*.bin *.iso *.cue *.chd *.cso *.gz *.elf *.irx *.gs *.gs.xz *.gs.zst *.dump);;"
"Single-Track Raw Images (*.bin *.iso);;"
"Cue Sheets (*.cue);;"
@ -78,14 +78,13 @@ static constexpr char OPEN_FILE_FILTER[] =
"GS Dumps (*.gs *.gs.xz *.gs.zst);;"
"Block Dumps (*.dump)");
static constexpr char DISC_IMAGE_FILTER[] =
QT_TRANSLATE_NOOP("MainWindow", "All File Types (*.bin *.iso *.cue *.chd *.cso *.gz *.dump);;"
"Single-Track Raw Images (*.bin *.iso);;"
"Cue Sheets (*.cue);;"
"MAME CHD Images (*.chd);;"
"CSO Images (*.cso);;"
"GZ Images (*.gz);;"
"Block Dumps (*.dump)");
const char* MainWindow::DISC_IMAGE_FILTER = QT_TRANSLATE_NOOP("MainWindow", "All File Types (*.bin *.iso *.cue *.chd *.cso *.gz *.dump);;"
"Single-Track Raw Images (*.bin *.iso);;"
"Cue Sheets (*.cue);;"
"MAME CHD Images (*.chd);;"
"CSO Images (*.cso);;"
"GZ Images (*.gz);;"
"Block Dumps (*.dump)");
#ifdef __APPLE__
const char* MainWindow::DEFAULT_THEME_NAME = "";
@ -283,9 +282,8 @@ void MainWindow::setupAdditionalUi()
raAction->setChecked(checked);
}
connect(raAction, &QAction::triggered, this, [id = id]() {
Host::RunOnCPUThread([id]() { Achievements::RAIntegration::ActivateMenuItem(id); }, false);
});
connect(raAction, &QAction::triggered, this,
[id = id]() { Host::RunOnCPUThread([id]() { Achievements::RAIntegration::ActivateMenuItem(id); }, false); });
}
});
m_ui.menu_Tools->insertMenu(m_ui.menuInput_Recording->menuAction(), raMenu);
@ -324,11 +322,12 @@ void MainWindow::connectSignals()
connect(m_ui.actionDEV9Settings, &QAction::triggered, [this]() { doSettings("Network & HDD"); });
connect(m_ui.actionFolderSettings, &QAction::triggered, [this]() { doSettings("Folders"); });
connect(m_ui.actionAchievementSettings, &QAction::triggered, [this]() { doSettings("Achievements"); });
connect(
m_ui.actionControllerSettings, &QAction::triggered, [this]() { doControllerSettings(ControllerSettingsDialog::Category::GlobalSettings); });
connect(m_ui.actionHotkeySettings, &QAction::triggered, [this]() { doControllerSettings(ControllerSettingsDialog::Category::HotkeySettings); });
connect(
m_ui.actionAddGameDirectory, &QAction::triggered, [this]() { getSettingsDialog()->getGameListSettingsWidget()->addSearchDirectory(this); });
connect(m_ui.actionControllerSettings, &QAction::triggered,
[this]() { doControllerSettings(ControllerSettingsDialog::Category::GlobalSettings); });
connect(m_ui.actionHotkeySettings, &QAction::triggered,
[this]() { doControllerSettings(ControllerSettingsDialog::Category::HotkeySettings); });
connect(m_ui.actionAddGameDirectory, &QAction::triggered,
[this]() { getSettingsDialog()->getGameListSettingsWidget()->addSearchDirectory(this); });
connect(m_ui.actionScanForNewGames, &QAction::triggered, [this]() { refreshGameList(false); });
connect(m_ui.actionRescanAllGames, &QAction::triggered, [this]() { refreshGameList(true); });
connect(m_ui.actionViewToolbar, &QAction::toggled, this, &MainWindow::onViewToolbarActionToggled);
@ -395,8 +394,8 @@ void MainWindow::connectSignals()
connect(m_game_list_widget, &GameListWidget::refreshComplete, this, &MainWindow::onGameListRefreshComplete);
connect(m_game_list_widget, &GameListWidget::selectionChanged, this, &MainWindow::onGameListSelectionChanged, Qt::QueuedConnection);
connect(m_game_list_widget, &GameListWidget::entryActivated, this, &MainWindow::onGameListEntryActivated, Qt::QueuedConnection);
connect(
m_game_list_widget, &GameListWidget::entryContextMenuRequested, this, &MainWindow::onGameListEntryContextMenuRequested, Qt::QueuedConnection);
connect(m_game_list_widget, &GameListWidget::entryContextMenuRequested, this, &MainWindow::onGameListEntryContextMenuRequested,
Qt::QueuedConnection);
connect(m_game_list_widget, &GameListWidget::addGameDirectoryRequested, this,
[this]() { getSettingsDialog()->getGameListSettingsWidget()->addSearchDirectory(this); });
}
@ -430,8 +429,8 @@ void MainWindow::connectVMThreadSignals(EmuThread* thread)
GSRendererType::OGL, GSRendererType::VK, GSRendererType::SW, GSRendererType::Null};
for (GSRendererType renderer : renderers)
{
connect(m_ui.menuDebugSwitchRenderer->addAction(QString::fromUtf8(Pcsx2Config::GSOptions::GetRendererName(renderer))), &QAction::triggered,
[renderer] { g_emu_thread->switchRenderer(renderer); });
connect(m_ui.menuDebugSwitchRenderer->addAction(QString::fromUtf8(Pcsx2Config::GSOptions::GetRendererName(renderer))),
&QAction::triggered, [renderer] { g_emu_thread->switchRenderer(renderer); });
}
}
@ -817,8 +816,7 @@ void MainWindow::onBlockDumpActionToggled(bool checked)
// prompt for a location to save
const QString new_dir(
QFileDialog::getExistingDirectory(this, tr("Select location to save block dump:"),
QString::fromStdString(old_directory)));
QFileDialog::getExistingDirectory(this, tr("Select location to save block dump:"), QString::fromStdString(old_directory)));
if (new_dir.isEmpty())
{
// disable it again
@ -839,11 +837,12 @@ void MainWindow::onShowAdvancedSettingsToggled(bool checked)
QCheckBox* cb = new QCheckBox(tr("Do not show again"));
QMessageBox mb(this);
mb.setWindowTitle(tr("Show Advanced Settings"));
mb.setText(
tr("Changing advanced settings can have unpredictable effects on games, including graphical glitches, lock-ups, and even corrupted save files. "
"We do not recommend changing advanced settings unless you know what you are doing, and the implications of changing each setting.\n\n"
"The PCSX2 team will not provide any support for configurations that modify these settings, you are on your own.\n\n"
"Are you sure you want to continue?"));
mb.setText(tr("Changing advanced settings can have unpredictable effects on games, including graphical glitches, lock-ups, and "
"even corrupted save files. "
"We do not recommend changing advanced settings unless you know what you are doing, and the implications of changing "
"each setting.\n\n"
"The PCSX2 team will not provide any support for configurations that modify these settings, you are on your own.\n\n"
"Are you sure you want to continue?"));
mb.setIcon(QMessageBox::Warning);
mb.addButton(QMessageBox::Yes);
mb.addButton(QMessageBox::No);
@ -1097,8 +1096,7 @@ bool MainWindow::shouldHideMainWindow() const
{
// NOTE: We can't use isRenderingToMain() here, because this happens post-fullscreen-switch.
return Host::GetBoolSettingValue("UI", "HideMainWindowWhenRunning", false) ||
(g_emu_thread->shouldRenderToMain() && isRenderingFullscreen()) ||
QtHost::InNoGUIMode();
(g_emu_thread->shouldRenderToMain() && isRenderingFullscreen()) || QtHost::InNoGUIMode();
}
void MainWindow::switchToGameListView()
@ -1173,7 +1171,8 @@ void MainWindow::runOnUIThread(const std::function<void()>& func)
func();
}
bool MainWindow::requestShutdown(bool allow_confirm /* = true */, bool allow_save_to_state /* = true */, bool default_save_to_state /* = true */, bool block_until_done /* = false */)
bool MainWindow::requestShutdown(bool allow_confirm /* = true */, bool allow_save_to_state /* = true */,
bool default_save_to_state /* = true */, bool block_until_done /* = false */)
{
if (!s_vm_valid)
return true;
@ -1310,8 +1309,8 @@ void MainWindow::onGameListEntryActivated()
// we might still be saving a resume state...
VMManager::WaitForSaveStateFlush();
const std::optional<bool> resume = promptForResumeState(
QString::fromStdString(VMManager::GetSaveStateFileName(entry->serial.c_str(), entry->crc, -1)));
const std::optional<bool> resume =
promptForResumeState(QString::fromStdString(VMManager::GetSaveStateFileName(entry->serial.c_str(), entry->crc, -1)));
if (!resume.has_value())
{
// cancelled
@ -1332,9 +1331,14 @@ void MainWindow::onGameListEntryContextMenuRequested(const QPoint& point)
if (entry)
{
QAction* action = menu.addAction(tr("Properties..."));
action->setEnabled(!entry->serial.empty());
action->setEnabled(!entry->serial.empty() || entry->type == GameList::EntryType::ELF);
if (action->isEnabled())
connect(action, &QAction::triggered, [entry]() { SettingsDialog::openGamePropertiesDialog(entry, entry->serial, entry->crc); });
{
connect(action, &QAction::triggered, [entry]() {
SettingsDialog::openGamePropertiesDialog(
entry, (entry->type != GameList::EntryType::ELF) ? std::string_view(entry->serial) : std::string_view(), entry->crc);
});
}
action = menu.addAction(tr("Open Containing Directory..."));
connect(action, &QAction::triggered, [this, entry]() {
@ -1348,8 +1352,7 @@ void MainWindow::onGameListEntryContextMenuRequested(const QPoint& point)
connect(menu.addAction(tr("Exclude From List")), &QAction::triggered,
[this, entry]() { getSettingsDialog()->getGameListSettingsWidget()->addExcludedPath(entry->path); });
connect(menu.addAction(tr("Reset Play Time")), &QAction::triggered,
[this, entry]() { clearGameListEntryPlayTime(entry); });
connect(menu.addAction(tr("Reset Play Time")), &QAction::triggered, [this, entry]() { clearGameListEntryPlayTime(entry); });
menu.addSeparator();
@ -1398,7 +1401,8 @@ void MainWindow::onGameListEntryContextMenuRequested(const QPoint& point)
void MainWindow::onStartFileActionTriggered()
{
const QString path(QDir::toNativeSeparators(QFileDialog::getOpenFileName(this, tr("Start File"), QString(), tr(OPEN_FILE_FILTER), nullptr)));
const QString path(
QDir::toNativeSeparators(QFileDialog::getOpenFileName(this, tr("Start File"), QString(), tr(OPEN_FILE_FILTER), nullptr)));
if (path.isEmpty())
return;
@ -1423,7 +1427,8 @@ void MainWindow::onStartBIOSActionTriggered()
void MainWindow::onChangeDiscFromFileActionTriggered()
{
VMLock lock(pauseAndLockVM());
QString filename = QFileDialog::getOpenFileName(lock.getDialogParent(), tr("Select Disc Image"), QString(), tr(DISC_IMAGE_FILTER), nullptr);
QString filename =
QFileDialog::getOpenFileName(lock.getDialogParent(), tr("Select Disc Image"), QString(), tr(DISC_IMAGE_FILTER), nullptr);
if (filename.isEmpty())
return;
@ -1455,7 +1460,9 @@ void MainWindow::onChangeDiscMenuAboutToShow()
// TODO: This is where we would populate the playlist if there is one.
}
void MainWindow::onChangeDiscMenuAboutToHide() {}
void MainWindow::onChangeDiscMenuAboutToHide()
{
}
void MainWindow::onLoadStateMenuAboutToShow()
{
@ -1514,13 +1521,16 @@ void MainWindow::onViewGamePropertiesActionTriggered()
return;
// prefer to use a game list entry, if we have one, that way the summary is populated
if (!m_current_disc_path.isEmpty())
if (!m_current_disc_path.isEmpty() || !m_current_elf_override.isEmpty())
{
auto lock = GameList::GetLock();
const GameList::Entry* entry = GameList::GetEntryForPath(m_current_disc_path.toUtf8().constData());
const GameList::Entry* entry = m_current_elf_override.isEmpty() ?
GameList::GetEntryForPath(m_current_disc_path.toUtf8().constData()) :
GameList::GetEntryForPath(m_current_elf_override.toUtf8().constData());
if (entry)
{
SettingsDialog::openGamePropertiesDialog(entry, entry->serial, entry->crc);
SettingsDialog::openGamePropertiesDialog(
entry, m_current_elf_override.isEmpty() ? std::string_view(entry->serial) : std::string_view(), entry->crc);
return;
}
}
@ -1571,11 +1581,10 @@ void MainWindow::checkForUpdates(bool display_message)
QString message;
#ifdef _WIN32
message =
tr("<p>Sorry, you are trying to update a PCSX2 version which is not an official GitHub release. To "
"prevent incompatibilities, the auto-updater is only enabled on official builds.</p>"
"<p>To obtain an official build, please download from the link below:</p>"
"<p><a href=\"https://pcsx2.net/downloads/\">https://pcsx2.net/downloads/</a></p>");
message = tr("<p>Sorry, you are trying to update a PCSX2 version which is not an official GitHub release. To "
"prevent incompatibilities, the auto-updater is only enabled on official builds.</p>"
"<p>To obtain an official build, please download from the link below:</p>"
"<p><a href=\"https://pcsx2.net/downloads/\">https://pcsx2.net/downloads/</a></p>");
#else
message = tr("Automatic updating is not supported on the current platform.");
#endif
@ -1646,21 +1655,18 @@ void MainWindow::onInputRecNewActionTriggered()
if (result == QDialog::Accepted)
{
Host::RunOnCPUThread([&, filePath = dlg.getFilePath(),
fromSavestate = dlg.getInputRecType() == InputRecording::Type::FROM_SAVESTATE,
authorName = dlg.getAuthorName()]() {
if (g_InputRecording.create(
filePath,
fromSavestate,
authorName))
{
QtHost::RunOnUIThread([&]() {
m_ui.actionInputRecNew->setEnabled(false);
m_ui.actionInputRecStop->setEnabled(true);
m_ui.actionReset->setEnabled(!g_InputRecording.isTypeSavestate());
});
}
});
Host::RunOnCPUThread(
[&, filePath = dlg.getFilePath(), fromSavestate = dlg.getInputRecType() == InputRecording::Type::FROM_SAVESTATE,
authorName = dlg.getAuthorName()]() {
if (g_InputRecording.create(filePath, fromSavestate, authorName))
{
QtHost::RunOnUIThread([&]() {
m_ui.actionInputRecNew->setEnabled(false);
m_ui.actionInputRecStop->setEnabled(true);
m_ui.actionReset->setEnabled(!g_InputRecording.isTypeSavestate());
});
}
});
}
if (wasRunning && !wasPaused)
@ -1700,9 +1706,7 @@ void MainWindow::onInputRecPlayActionTriggered()
{
if (g_InputRecording.isActive())
{
Host::RunOnCPUThread([]() {
g_InputRecording.stop();
});
Host::RunOnCPUThread([]() { g_InputRecording.stop(); });
m_ui.actionInputRecStop->setEnabled(false);
}
Host::RunOnCPUThread([&, filename = fileNames.first().toStdString()]() {
@ -1852,9 +1856,10 @@ void MainWindow::onVMStopped()
m_game_list_widget->refresh(false);
}
void MainWindow::onGameChanged(const QString& path, const QString& serial, const QString& name, quint32 crc)
void MainWindow::onGameChanged(const QString& path, const QString& elf_override, const QString& serial, const QString& name, quint32 crc)
{
m_current_disc_path = path;
m_current_elf_override = elf_override;
m_current_game_serial = serial;
m_current_game_name = name;
m_current_game_crc = crc;
@ -1950,8 +1955,8 @@ void MainWindow::registerForDeviceNotifications()
#ifdef _WIN32
// We use these notifications to detect when a controller is connected or disconnected.
DEV_BROADCAST_DEVICEINTERFACE_W filter = {sizeof(DEV_BROADCAST_DEVICEINTERFACE_W), DBT_DEVTYP_DEVICEINTERFACE};
m_device_notification_handle = RegisterDeviceNotificationW((HANDLE)winId(), &filter,
DEVICE_NOTIFY_WINDOW_HANDLE | DEVICE_NOTIFY_ALL_INTERFACE_CLASSES);
m_device_notification_handle =
RegisterDeviceNotificationW((HANDLE)winId(), &filter, DEVICE_NOTIFY_WINDOW_HANDLE | DEVICE_NOTIFY_ALL_INTERFACE_CLASSES);
#endif
}
@ -2041,8 +2046,8 @@ DisplayWidget* MainWindow::createDisplay(bool fullscreen, bool render_to_main)
DisplayWidget* MainWindow::updateDisplay(bool fullscreen, bool render_to_main, bool surfaceless)
{
DevCon.WriteLn("updateDisplay() fullscreen=%s render_to_main=%s surfaceless=%s",
fullscreen ? "true" : "false", render_to_main ? "true" : "false", surfaceless ? "true" : "false");
DevCon.WriteLn("updateDisplay() fullscreen=%s render_to_main=%s surfaceless=%s", fullscreen ? "true" : "false",
render_to_main ? "true" : "false", surfaceless ? "true" : "false");
QWidget* container = m_display_container ? static_cast<QWidget*>(m_display_container) : static_cast<QWidget*>(m_display_widget);
const bool is_fullscreen = isRenderingFullscreen();
@ -2057,7 +2062,8 @@ DisplayWidget* MainWindow::updateDisplay(bool fullscreen, bool render_to_main, b
// .. except on Wayland, where everything tends to break if you don't recreate.
const bool has_container = (m_display_container != nullptr);
const bool needs_container = DisplayContainer::isNeeded(fullscreen, render_to_main);
if (!is_rendering_to_main && !render_to_main && !is_exclusive_fullscreen && has_container == needs_container && !needs_container && !changing_surfaceless)
if (!is_rendering_to_main && !render_to_main && !is_exclusive_fullscreen && has_container == needs_container && !needs_container &&
!changing_surfaceless)
{
DevCon.WriteLn("Toggling to %s without recreating surface", (fullscreen ? "fullscreen" : "windowed"));
if (g_host_display->IsFullscreen())
@ -2351,8 +2357,7 @@ SettingsDialog* MainWindow::getSettingsDialog()
if (!m_settings_dialog)
{
m_settings_dialog = new SettingsDialog(this);
connect(
m_settings_dialog->getInterfaceSettingsWidget(), &InterfaceSettingsWidget::themeChanged, this, &MainWindow::updateTheme);
connect(m_settings_dialog->getInterfaceSettingsWidget(), &InterfaceSettingsWidget::themeChanged, this, &MainWindow::updateTheme);
}
return m_settings_dialog;
@ -2454,14 +2459,16 @@ void MainWindow::startGameListEntry(const GameList::Entry* entry, std::optional<
void MainWindow::setGameListEntryCoverImage(const GameList::Entry* entry)
{
const QString filename(QFileDialog::getOpenFileName(this, tr("Select Cover Image"), QString(), tr("All Cover Image Types (*.jpg *.jpeg *.png)")));
const QString filename(
QFileDialog::getOpenFileName(this, tr("Select Cover Image"), QString(), tr("All Cover Image Types (*.jpg *.jpeg *.png)")));
if (filename.isEmpty())
return;
if (!GameList::GetCoverImagePathForEntry(entry).empty())
{
if (QMessageBox::question(this, tr("Cover Already Exists"), tr("A cover image for this game already exists, do you wish to replace it?"),
QMessageBox::Yes, QMessageBox::No) != QMessageBox::Yes)
if (QMessageBox::question(this, tr("Cover Already Exists"),
tr("A cover image for this game already exists, do you wish to replace it?"), QMessageBox::Yes,
QMessageBox::No) != QMessageBox::Yes)
{
return;
}
@ -2645,10 +2652,8 @@ void MainWindow::populateLoadStateMenu(QMenu* menu, const QString& filename, con
if (has_any_states)
{
connect(delete_save_states_action, &QAction::triggered, this, [this, serial, crc] {
if (QMessageBox::warning(
this, tr("Delete Save States"),
tr("Are you sure you want to delete all save states for %1?\n\nThe saves will not be recoverable.")
.arg(serial),
if (QMessageBox::warning(this, tr("Delete Save States"),
tr("Are you sure you want to delete all save states for %1?\n\nThe saves will not be recoverable.").arg(serial),
QMessageBox::Yes, QMessageBox::No) != QMessageBox::Yes)
{
return;
@ -2721,8 +2726,7 @@ void MainWindow::doStartFile(std::optional<CDVD_SourceType> source, const QStrin
VMManager::WaitForSaveStateFlush();
const std::optional<bool> resume(
promptForResumeState(
QString::fromStdString(VMManager::GetSaveStateFileName(params->filename.c_str(), -1))));
promptForResumeState(QString::fromStdString(VMManager::GetSaveStateFileName(params->filename.c_str(), -1))));
if (!resume.has_value())
return;
else if (resume.value())
@ -2736,8 +2740,8 @@ void MainWindow::doDiscChange(CDVD_SourceType source, const QString& path)
bool reset_system = false;
if (!m_was_disc_change_request)
{
const int choice = QMessageBox::question(this, tr("Confirm Disc Change"), tr("Do you want to swap discs or boot the new image (via system reset)?"),
tr("Swap Disc"), tr("Reset"), tr("Cancel"), 0, 2);
const int choice = QMessageBox::question(this, tr("Confirm Disc Change"),
tr("Do you want to swap discs or boot the new image (via system reset)?"), tr("Swap Disc"), tr("Reset"), tr("Cancel"), 0, 2);
if (choice == 2)
return;
reset_system = (choice != 0);
@ -2769,6 +2773,11 @@ MainWindow::VMLock MainWindow::pauseAndLockVM()
return VMLock(dialog_parent, was_paused, was_fullscreen);
}
void MainWindow::rescanFile(const std::string& path)
{
m_game_list_widget->rescanFile(path);
}
MainWindow::VMLock::VMLock(QWidget* dialog_parent, bool was_paused, bool was_fullscreen)
: m_dialog_parent(dialog_parent)
, m_was_paused(was_paused)

View File

@ -78,6 +78,12 @@ public:
/// Default theme name for the platform.
static const char* DEFAULT_THEME_NAME;
/// Default filter for opening a file.
static const char* OPEN_FILE_FILTER;
/// Default filter for opening a disc image.
static const char* DISC_IMAGE_FILTER;
public:
MainWindow();
~MainWindow();
@ -100,6 +106,9 @@ public:
__fi QLabel* getStatusFPSWidget() const { return m_status_fps_widget; }
__fi QLabel* getStatusVPSWidget() const { return m_status_vps_widget; }
/// Rescans a single file. NOTE: Happens on UI thread.
void rescanFile(const std::string& path);
public Q_SLOTS:
void checkForUpdates(bool display_message);
void refreshGameList(bool invalidate_cache);
@ -108,7 +117,8 @@ public Q_SLOTS:
void reportError(const QString& title, const QString& message);
bool confirmMessage(const QString& title, const QString& message);
void runOnUIThread(const std::function<void()>& func);
bool requestShutdown(bool allow_confirm = true, bool allow_save_to_state = true, bool default_save_to_state = true, bool block_until_done = false);
bool requestShutdown(
bool allow_confirm = true, bool allow_save_to_state = true, bool default_save_to_state = true, bool block_until_done = false);
void requestExit();
void checkForSettingChanges();
std::optional<WindowInfo> getWindowInfo();
@ -172,7 +182,7 @@ private Q_SLOTS:
void onVMResumed();
void onVMStopped();
void onGameChanged(const QString& path, const QString& serial, const QString& name, quint32 crc);
void onGameChanged(const QString& path, const QString& elf_override, const QString& serial, const QString& name, quint32 crc);
protected:
void showEvent(QShowEvent* event) override;
@ -234,8 +244,8 @@ private:
QString getDiscDevicePath(const QString& title);
void startGameListEntry(const GameList::Entry* entry, std::optional<s32> save_slot = std::nullopt,
std::optional<bool> fast_boot = std::nullopt);
void startGameListEntry(
const GameList::Entry* entry, std::optional<s32> save_slot = std::nullopt, std::optional<bool> fast_boot = std::nullopt);
void setGameListEntryCoverImage(const GameList::Entry* entry);
void clearGameListEntryPlayTime(const GameList::Entry* entry);
@ -267,6 +277,7 @@ private:
QLabel* m_status_resolution_widget = nullptr;
QString m_current_disc_path;
QString m_current_elf_override;
QString m_current_game_serial;
QString m_current_game_name;
quint32 m_current_game_crc;

View File

@ -74,15 +74,16 @@ EmuThread* g_emu_thread = nullptr;
//////////////////////////////////////////////////////////////////////////
// Local function declarations
//////////////////////////////////////////////////////////////////////////
namespace QtHost {
static void PrintCommandLineVersion();
static void PrintCommandLineHelp(const std::string_view& progname);
static std::shared_ptr<VMBootParameters>& AutoBoot(std::shared_ptr<VMBootParameters>& autoboot);
static bool ParseCommandLineOptions(const QStringList& args, std::shared_ptr<VMBootParameters>& autoboot);
static bool InitializeConfig();
static void SaveSettings();
static void HookSignals();
}
namespace QtHost
{
static void PrintCommandLineVersion();
static void PrintCommandLineHelp(const std::string_view& progname);
static std::shared_ptr<VMBootParameters>& AutoBoot(std::shared_ptr<VMBootParameters>& autoboot);
static bool ParseCommandLineOptions(const QStringList& args, std::shared_ptr<VMBootParameters>& autoboot);
static bool InitializeConfig();
static void SaveSettings();
static void HookSignals();
} // namespace QtHost
//////////////////////////////////////////////////////////////////////////
// Local variable declarations
@ -151,8 +152,8 @@ bool EmuThread::confirmMessage(const QString& title, const QString& message)
{
// This is definitely deadlock risky, but unlikely to happen (why would GS be confirming?).
bool result = false;
QMetaObject::invokeMethod(g_emu_thread, "confirmMessage", Qt::BlockingQueuedConnection,
Q_RETURN_ARG(bool, result), Q_ARG(const QString&, title), Q_ARG(const QString&, message));
QMetaObject::invokeMethod(g_emu_thread, "confirmMessage", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, result),
Q_ARG(const QString&, title), Q_ARG(const QString&, message));
return result;
}
@ -231,8 +232,7 @@ void EmuThread::startVM(std::shared_ptr<VMBootParameters> boot_params)
{
if (!isOnEmuThread())
{
QMetaObject::invokeMethod(this, "startVM", Qt::QueuedConnection,
Q_ARG(std::shared_ptr<VMBootParameters>, boot_params));
QMetaObject::invokeMethod(this, "startVM", Qt::QueuedConnection, Q_ARG(std::shared_ptr<VMBootParameters>, boot_params));
return;
}
@ -460,9 +460,8 @@ void EmuThread::startBackgroundControllerPollTimer()
if (m_background_controller_polling_timer->isActive())
return;
m_background_controller_polling_timer->start(FullscreenUI::IsInitialized() ?
FULLSCREEN_UI_CONTROLLER_POLLING_INTERVAL :
BACKGROUND_CONTROLLER_POLLING_INTERVAL);
m_background_controller_polling_timer->start(
FullscreenUI::IsInitialized() ? FULLSCREEN_UI_CONTROLLER_POLLING_INTERVAL : BACKGROUND_CONTROLLER_POLLING_INTERVAL);
}
void EmuThread::stopBackgroundControllerPollTimer()
@ -845,9 +844,7 @@ void EmuThread::queueSnapshot(quint32 gsdump_frames)
if (!VMManager::HasValidVM())
return;
GetMTGS().RunOnGSThread([gsdump_frames]() {
GSQueueSnapshot(std::string(), gsdump_frames);
});
GetMTGS().RunOnGSThread([gsdump_frames]() { GSQueueSnapshot(std::string(), gsdump_frames); });
}
void EmuThread::updateDisplay()
@ -1014,10 +1011,12 @@ void Host::OnVMResumed()
emit g_emu_thread->onVMResumed();
}
void Host::OnGameChanged(const std::string& disc_path, const std::string& game_serial, const std::string& game_name, u32 game_crc)
void Host::OnGameChanged(const std::string& disc_path, const std::string& elf_override, const std::string& game_serial,
const std::string& game_name, u32 game_crc)
{
CommonHost::OnGameChanged(disc_path, game_serial, game_name, game_crc);
emit g_emu_thread->onGameChanged(QString::fromStdString(disc_path), QString::fromStdString(game_serial), QString::fromStdString(game_name), game_crc);
CommonHost::OnGameChanged(disc_path, elf_override, game_serial, game_name, game_crc);
emit g_emu_thread->onGameChanged(QString::fromStdString(disc_path), QString::fromStdString(elf_override),
QString::fromStdString(game_serial), QString::fromStdString(game_name), game_crc);
}
void EmuThread::updatePerformanceMetrics(bool force)
@ -1030,12 +1029,11 @@ void EmuThread::updatePerformanceMetrics(bool force)
QString gs_stat;
if (THREAD_VU1)
{
gs_stat =
QStringLiteral("%1 | EE: %2% | VU: %3% | GS: %4%")
.arg(gs_stat_str.c_str())
.arg(PerformanceMetrics::GetCPUThreadUsage(), 0, 'f', 0)
.arg(PerformanceMetrics::GetVUThreadUsage(), 0, 'f', 0)
.arg(PerformanceMetrics::GetGSThreadUsage(), 0, 'f', 0);
gs_stat = QStringLiteral("%1 | EE: %2% | VU: %3% | GS: %4%")
.arg(gs_stat_str.c_str())
.arg(PerformanceMetrics::GetCPUThreadUsage(), 0, 'f', 0)
.arg(PerformanceMetrics::GetVUThreadUsage(), 0, 'f', 0)
.arg(PerformanceMetrics::GetGSThreadUsage(), 0, 'f', 0);
}
else
{
@ -1055,17 +1053,18 @@ void EmuThread::updatePerformanceMetrics(bool force)
int iwidth, iheight;
GSgetInternalResolution(&iwidth, &iheight);
if (iwidth != m_last_internal_width || iheight != m_last_internal_height ||
speed != m_last_speed || gfps != m_last_game_fps || vfps != m_last_video_fps ||
renderer != m_last_renderer || force)
if (iwidth != m_last_internal_width || iheight != m_last_internal_height || speed != m_last_speed || gfps != m_last_game_fps ||
vfps != m_last_video_fps || renderer != m_last_renderer || force)
{
if (iwidth == 0 && iheight == 0)
{
// if we don't have width/height yet, we're not going to have fps either.
// and we'll probably be <100% due to compiling. so just leave it blank for now.
QString blank;
QMetaObject::invokeMethod(g_main_window->getStatusRendererWidget(), "setText", Qt::QueuedConnection, Q_ARG(const QString&, blank));
QMetaObject::invokeMethod(g_main_window->getStatusResolutionWidget(), "setText", Qt::QueuedConnection, Q_ARG(const QString&, blank));
QMetaObject::invokeMethod(
g_main_window->getStatusRendererWidget(), "setText", Qt::QueuedConnection, Q_ARG(const QString&, blank));
QMetaObject::invokeMethod(
g_main_window->getStatusResolutionWidget(), "setText", Qt::QueuedConnection, Q_ARG(const QString&, blank));
QMetaObject::invokeMethod(g_main_window->getStatusFPSWidget(), "setText", Qt::QueuedConnection, Q_ARG(const QString&, blank));
QMetaObject::invokeMethod(g_main_window->getStatusVPSWidget(), "setText", Qt::QueuedConnection, Q_ARG(const QString&, blank));
return;
@ -1081,9 +1080,7 @@ void EmuThread::updatePerformanceMetrics(bool force)
if (iwidth != m_last_internal_width || iheight != m_last_internal_height || force)
{
QMetaObject::invokeMethod(g_main_window->getStatusResolutionWidget(), "setText", Qt::QueuedConnection,
Q_ARG(const QString&, tr("%1x%2")
.arg(iwidth)
.arg(iheight)));
Q_ARG(const QString&, tr("%1x%2").arg(iwidth).arg(iheight)));
m_last_internal_width = iwidth;
m_last_internal_height = iheight;
}
@ -1091,17 +1088,14 @@ void EmuThread::updatePerformanceMetrics(bool force)
if (gfps != m_last_game_fps || force)
{
QMetaObject::invokeMethod(g_main_window->getStatusFPSWidget(), "setText", Qt::QueuedConnection,
Q_ARG(const QString&, tr("Game: %1 FPS")
.arg(gfps, 0, 'f', 0)));
Q_ARG(const QString&, tr("Game: %1 FPS").arg(gfps, 0, 'f', 0)));
m_last_game_fps = gfps;
}
if (speed != m_last_speed || vfps != m_last_video_fps || force)
{
QMetaObject::invokeMethod(g_main_window->getStatusVPSWidget(), "setText", Qt::QueuedConnection,
Q_ARG(const QString&, tr("Video: %1 FPS (%2%)")
.arg(vfps, 0, 'f', 0)
.arg(speed, 0, 'f', 0)));
Q_ARG(const QString&, tr("Video: %1 FPS (%2%)").arg(vfps, 0, 'f', 0).arg(speed, 0, 'f', 0)));
m_last_speed = speed;
m_last_video_fps = vfps;
}
@ -1144,10 +1138,9 @@ void Host::OnAchievementsRefreshed()
achievement_count = Achievements::GetAchievementCount();
max_points = Achievements::GetMaximumPointsForGame();
game_info = qApp->translate("EmuThread",
"Game ID: %1\n"
"Game Title: %2\n"
"Achievements: %5 (%6)\n\n")
game_info = qApp->translate("EmuThread", "Game ID: %1\n"
"Game Title: %2\n"
"Achievements: %5 (%6)\n\n")
.arg(game_id)
.arg(QString::fromStdString(Achievements::GetGameTitle()))
.arg(achievement_count)
@ -1183,15 +1176,13 @@ void Host::RunOnCPUThread(std::function<void()> function, bool block /* = false
return;
}
QMetaObject::invokeMethod(g_emu_thread, "runOnCPUThread",
block ? Qt::BlockingQueuedConnection : Qt::QueuedConnection,
QMetaObject::invokeMethod(g_emu_thread, "runOnCPUThread", block ? Qt::BlockingQueuedConnection : Qt::QueuedConnection,
Q_ARG(const std::function<void()>&, std::move(function)));
}
void Host::RefreshGameListAsync(bool invalidate_cache)
{
QMetaObject::invokeMethod(g_main_window, "refreshGameList", Qt::QueuedConnection,
Q_ARG(bool, invalidate_cache));
QMetaObject::invokeMethod(g_main_window, "refreshGameList", Qt::QueuedConnection, Q_ARG(bool, invalidate_cache));
}
void Host::CancelGameListRefresh()
@ -1213,9 +1204,8 @@ void Host::RequestVMShutdown(bool allow_confirm, bool allow_save_state, bool def
return;
// Run it on the host thread, that way we get the confirm prompt (if enabled).
QMetaObject::invokeMethod(g_main_window, "requestShutdown", Qt::QueuedConnection,
Q_ARG(bool, allow_confirm), Q_ARG(bool, allow_save_state),
Q_ARG(bool, default_save_state), Q_ARG(bool, false));
QMetaObject::invokeMethod(g_main_window, "requestShutdown", Qt::QueuedConnection, Q_ARG(bool, allow_confirm),
Q_ARG(bool, allow_save_state), Q_ARG(bool, default_save_state), Q_ARG(bool, false));
}
bool Host::IsFullscreen()
@ -1254,7 +1244,8 @@ bool QtHost::InitializeConfig()
// If the config file doesn't exist, assume this is a new install and don't prompt to overwrite.
if (FileSystem::FileExists(s_base_settings_interface->GetFileName().c_str()) &&
QMessageBox::question(nullptr, QStringLiteral("PCSX2"),
QStringLiteral("Settings failed to load, or are the incorrect version. Clicking Yes will reset all settings to defaults. Do you want to continue?")) != QMessageBox::Yes)
QStringLiteral("Settings failed to load, or are the incorrect version. Clicking Yes will reset all settings to defaults. "
"Do you want to continue?")) != QMessageBox::Yes)
{
return false;
}
@ -1339,8 +1330,7 @@ bool QtHost::ShouldShowAdvancedSettings()
void QtHost::RunOnUIThread(const std::function<void()>& func, bool block /*= false*/)
{
// main window always exists, so it's fine to attach it to that.
QMetaObject::invokeMethod(g_main_window, "runOnUIThread",
block ? Qt::BlockingQueuedConnection : Qt::QueuedConnection,
QMetaObject::invokeMethod(g_main_window, "runOnUIThread", block ? Qt::BlockingQueuedConnection : Qt::QueuedConnection,
Q_ARG(const std::function<void()>&, func));
}
@ -1369,10 +1359,8 @@ QString QtHost::GetAppNameAndVersion()
else if constexpr (PCSX2_isReleaseVersion)
{
#define APPNAME_STRINGIZE(x) #x
ret = QStringLiteral("PCSX2 "
APPNAME_STRINGIZE(PCSX2_VersionHi) "."
APPNAME_STRINGIZE(PCSX2_VersionMid) "."
APPNAME_STRINGIZE(PCSX2_VersionLo));
ret = QStringLiteral(
"PCSX2 " APPNAME_STRINGIZE(PCSX2_VersionHi) "." APPNAME_STRINGIZE(PCSX2_VersionMid) "." APPNAME_STRINGIZE(PCSX2_VersionLo));
#undef APPNAME_STRINGIZE
}
else
@ -1431,14 +1419,12 @@ void Host::ReportErrorAsync(const std::string_view& title, const std::string_vie
{
if (!title.empty() && !message.empty())
{
Console.Error("ReportErrorAsync: %.*s: %.*s",
static_cast<int>(title.size()), title.data(),
static_cast<int>(message.size()), message.data());
Console.Error(
"ReportErrorAsync: %.*s: %.*s", static_cast<int>(title.size()), title.data(), static_cast<int>(message.size()), message.data());
}
else if (!message.empty())
{
Console.Error("ReportErrorAsync: %.*s",
static_cast<int>(message.size()), message.data());
Console.Error("ReportErrorAsync: %.*s", static_cast<int>(message.size()), message.data());
}
QMetaObject::invokeMethod(g_main_window, "reportError", Qt::QueuedConnection,
@ -1455,9 +1441,7 @@ bool Host::ConfirmMessage(const std::string_view& title, const std::string_view&
void Host::OpenURL(const std::string_view& url)
{
QtHost::RunOnUIThread([url = QtUtils::StringViewToQString(url)]() {
QtUtils::OpenURL(g_main_window, QUrl(url));
});
QtHost::RunOnUIThread([url = QtUtils::StringViewToQString(url)]() { QtUtils::OpenURL(g_main_window, QUrl(url)); });
}
bool Host::CopyTextToClipboard(const std::string_view& text)
@ -1493,15 +1477,13 @@ std::optional<WindowInfo> Host::GetTopLevelWindowInfo()
void Host::OnInputDeviceConnected(const std::string_view& identifier, const std::string_view& device_name)
{
emit g_emu_thread->onInputDeviceConnected(
identifier.empty() ? QString() : QString::fromUtf8(identifier.data(), identifier.size()),
emit g_emu_thread->onInputDeviceConnected(identifier.empty() ? QString() : QString::fromUtf8(identifier.data(), identifier.size()),
device_name.empty() ? QString() : QString::fromUtf8(device_name.data(), device_name.size()));
}
void Host::OnInputDeviceDisconnected(const std::string_view& identifier)
{
emit g_emu_thread->onInputDeviceDisconnected(
identifier.empty() ? QString() : QString::fromUtf8(identifier.data(), identifier.size()));
emit g_emu_thread->onInputDeviceDisconnected(identifier.empty() ? QString() : QString::fromUtf8(identifier.data(), identifier.size()));
}
//////////////////////////////////////////////////////////////////////////
@ -1746,9 +1728,9 @@ bool QtHost::ParseCommandLineOptions(const QStringList& args, std::shared_ptr<VM
// scanning the game list).
if (s_batch_mode && !s_start_fullscreen_ui && !autoboot)
{
QMessageBox::critical(nullptr, QStringLiteral("Error"), s_nogui_mode ?
QStringLiteral("Cannot use no-gui mode, because no boot filename was specified.") :
QStringLiteral("Cannot use batch mode, because no boot filename was specified."));
QMessageBox::critical(nullptr, QStringLiteral("Error"),
s_nogui_mode ? QStringLiteral("Cannot use no-gui mode, because no boot filename was specified.") :
QStringLiteral("Cannot use batch mode, because no boot filename was specified."));
return false;
}

View File

@ -137,7 +137,7 @@ Q_SIGNALS:
void onVMStopped();
/// Provided by the host; called when the running executable changes.
void onGameChanged(const QString& path, const QString& serial, const QString& name, quint32 crc);
void onGameChanged(const QString& path, const QString& elf_override, const QString& serial, const QString& name, quint32 crc);
void onInputDevicesEnumerated(const QList<QPair<QString, QString>>& devices);
void onInputDeviceConnected(const QString& identifier, const QString& device_name);

View File

@ -22,7 +22,14 @@
#include "GameSummaryWidget.h"
#include "SettingsDialog.h"
#include "MainWindow.h"
#include "QtHost.h"
#include "QtUtils.h"
#include <QtCore/QDir>
#include <QtWidgets/QFileDialog>
#include "fmt/format.h"
GameSummaryWidget::GameSummaryWidget(const GameList::Entry* entry, SettingsDialog* dialog, QWidget* parent)
: m_dialog(dialog)
@ -32,35 +39,40 @@ GameSummaryWidget::GameSummaryWidget(const GameList::Entry* entry, SettingsDialo
const QString base_path(QtHost::GetResourcesBasePath());
for (int i = 0; i < m_ui.region->count(); i++)
{
m_ui.region->setItemIcon(i, QIcon(
QStringLiteral("%1/icons/flags/%2.png").arg(base_path).arg(GameList::RegionToString(static_cast<GameList::Region>(i)))));
m_ui.region->setItemIcon(i,
QIcon(QStringLiteral("%1/icons/flags/%2.png").arg(base_path).arg(GameList::RegionToString(static_cast<GameList::Region>(i)))));
}
for (int i = 1; i < m_ui.compatibility->count(); i++)
{
m_ui.compatibility->setItemIcon(i, QIcon(
QStringLiteral("%1/icons/star-%2.png").arg(base_path).arg(i)));
m_ui.compatibility->setItemIcon(i, QIcon(QStringLiteral("%1/icons/star-%2.png").arg(base_path).arg(i)));
}
populateUi(entry);
m_entry_path = entry->path;
populateInputProfiles();
populateDetails(entry);
populateDiscPath(entry);
connect(m_ui.inputProfile, &QComboBox::currentIndexChanged, this, &GameSummaryWidget::onInputProfileChanged);
}
GameSummaryWidget::~GameSummaryWidget() = default;
void GameSummaryWidget::populateUi(const GameList::Entry* entry)
void GameSummaryWidget::populateInputProfiles()
{
for (const std::string& name : PAD::GetInputProfileNames())
m_ui.inputProfile->addItem(QString::fromStdString(name));
}
void GameSummaryWidget::populateDetails(const GameList::Entry* entry)
{
m_ui.title->setText(QString::fromStdString(entry->title));
m_ui.path->setText(QString::fromStdString(entry->path));
m_ui.serial->setText(QString::fromStdString(entry->serial));
m_ui.crc->setText(QString::fromStdString(StringUtil::StdStringFromFormat("%08X", entry->crc)));
m_ui.crc->setText(QString::fromStdString(fmt::format("{:08X}", entry->crc)));
m_ui.type->setCurrentIndex(static_cast<int>(entry->type));
m_ui.region->setCurrentIndex(static_cast<int>(entry->region));
m_ui.compatibility->setCurrentIndex(static_cast<int>(entry->compatibility_rating));
for (const std::string& name : PAD::GetInputProfileNames())
m_ui.inputProfile->addItem(QString::fromStdString(name));
std::optional<std::string> profile(m_dialog->getStringValue("EmuCore", "InputProfileName", std::nullopt));
if (profile.has_value())
m_ui.inputProfile->setCurrentIndex(m_ui.inputProfile->findText(QString::fromStdString(profile.value())));
@ -68,6 +80,28 @@ void GameSummaryWidget::populateUi(const GameList::Entry* entry)
m_ui.inputProfile->setCurrentIndex(0);
}
void GameSummaryWidget::populateDiscPath(const GameList::Entry* entry)
{
if (entry->type == GameList::EntryType::ELF)
{
std::optional<std::string> iso_path(m_dialog->getStringValue("EmuCore", "DiscPath", std::nullopt));
if (!iso_path->empty())
m_ui.discPath->setText(QString::fromStdString(iso_path.value()));
connect(m_ui.discPath, &QLineEdit::textChanged, this, &GameSummaryWidget::onDiscPathChanged);
connect(m_ui.discPathBrowse, &QPushButton::clicked, this, &GameSummaryWidget::onDiscPathBrowseClicked);
connect(m_ui.discPathClear, &QPushButton::clicked, m_ui.discPath, &QLineEdit::clear);
}
else
{
// Makes no sense to have disc override for a disc.
m_ui.detailsFormLayout->removeRow(8);
m_ui.discPath = nullptr;
m_ui.discPathBrowse = nullptr;
m_ui.discPathClear = nullptr;
}
}
void GameSummaryWidget::onInputProfileChanged(int index)
{
if (index == 0)
@ -75,3 +109,31 @@ void GameSummaryWidget::onInputProfileChanged(int index)
else
m_dialog->setStringSettingValue("EmuCore", "InputProfileName", m_ui.inputProfile->itemText(index).toUtf8());
}
void GameSummaryWidget::onDiscPathChanged(const QString& value)
{
if (value.isEmpty())
m_dialog->removeSettingValue("EmuCore", "DiscPath");
else
m_dialog->setStringSettingValue("EmuCore", "DiscPath", value.toStdString().c_str());
// force rescan of elf to update the serial
g_main_window->rescanFile(m_entry_path);
// and re-fill our details (mainly the serial)
auto lock = GameList::GetLock();
const GameList::Entry* entry = GameList::GetEntryForPath(m_entry_path.c_str());
if (entry)
populateDetails(entry);
}
void GameSummaryWidget::onDiscPathBrowseClicked()
{
const QString filename(QFileDialog::getOpenFileName(
QtUtils::GetRootWidget(this), tr("Select Disc Path"), QString(), qApp->translate("MainWindow", MainWindow::DISC_IMAGE_FILTER)));
if (filename.isEmpty())
return;
// let the signal take care of it
m_ui.discPath->setText(QDir::toNativeSeparators(filename));
}

View File

@ -35,10 +35,15 @@ public:
~GameSummaryWidget();
private:
void populateUi(const GameList::Entry* entry);
void populateInputProfiles();
void populateDetails(const GameList::Entry* entry);
void populateDiscPath(const GameList::Entry* entry);
void onInputProfileChanged(int index);
void onDiscPathChanged(const QString& value);
void onDiscPathBrowseClicked();
Ui::GameSummaryWidget m_ui;
SettingsDialog* m_dialog;
std::string m_entry_path;
};

View File

@ -10,7 +10,7 @@
<height>562</height>
</rect>
</property>
<layout class="QFormLayout" name="formLayout">
<layout class="QFormLayout" name="detailsFormLayout">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::ExpandingFieldsGrow</enum>
</property>
@ -353,18 +353,12 @@
</item>
</widget>
</item>
<item row="8" column="1">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
<item row="7" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Input Profile:</string>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</widget>
</item>
<item row="7" column="1">
<widget class="QComboBox" name="inputProfile">
@ -381,13 +375,47 @@
</item>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_6">
<item row="8" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Input Profile:</string>
<string>Disc Path:</string>
</property>
</widget>
</item>
<item row="8" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLineEdit" name="discPath"/>
</item>
<item>
<widget class="QPushButton" name="discPathBrowse">
<property name="text">
<string>Browse...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="discPathClear">
<property name="text">
<string>Clear</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="9" column="1">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources>

View File

@ -469,6 +469,22 @@ void SettingsDialog::setStringSettingValue(const char* section, const char* key,
}
}
void SettingsDialog::removeSettingValue(const char* section, const char* key)
{
if (m_sif)
{
m_sif->DeleteValue(section, key);
m_sif->Save();
g_emu_thread->reloadGameSettings();
}
else
{
Host::RemoveBaseSettingValue(section, key);
Host::CommitBaseSettingChanges();
g_emu_thread->applySettings();
}
}
void SettingsDialog::openGamePropertiesDialog(const GameList::Entry* game, const std::string_view& serial, u32 crc)
{
// check for an existing dialog with this crc

View File

@ -90,6 +90,7 @@ public:
void setIntSettingValue(const char* section, const char* key, std::optional<int> value);
void setFloatSettingValue(const char* section, const char* key, std::optional<float> value);
void setStringSettingValue(const char* section, const char* key, std::optional<const char*> value);
void removeSettingValue(const char* section, const char* key);
Q_SIGNALS:
void settingsResetToDefaults();

View File

@ -433,6 +433,25 @@ static __fi void _reloadElfInfo(std::string elfpath)
// binary).
}
u32 cdvdGetElfCRC(const std::string& path)
{
try
{
// Yay for write-after-read here. Isn't our ELF parser great....
const s64 host_size = FileSystem::GetPathFileSize(path.c_str());
if (host_size <= 0)
return 0;
std::unique_ptr<ElfObject> elfptr(std::make_unique<ElfObject>(path, static_cast<u32>(std::max<s64>(host_size, 0)), false));
elfptr->loadHeaders();
return elfptr->getCRC();
}
catch ([[maybe_unused]] Exception::FileNotFound& e)
{
return 0;
}
}
static std::string ExecutablePathToSerial(const std::string& path)
{
// cdrom:\SCES_123.45;1
@ -490,16 +509,17 @@ void cdvdReloadElfInfo(std::string elfoverride)
DevCon.WriteLn(Color_Green, "Reload ELF");
try
{
std::string elfpath;
u32 discType = GetPS2ElfName(elfpath);
DiscSerial = ExecutablePathToSerial(elfpath);
// Use the serial from the disc (if any), and the ELF CRC of the override.
if (!elfoverride.empty())
{
_reloadElfInfo(std::move(elfoverride));
return;
}
std::string elfpath;
u32 discType = GetPS2ElfName(elfpath);
DiscSerial = ExecutablePathToSerial(elfpath);
if (discType == 1)
{
// PCSX2 currently only recognizes *.elf executables in proper PS2 format.

View File

@ -176,6 +176,7 @@ extern u8 cdvdRead(u8 key);
extern void cdvdWrite(u8 key, u8 rt);
extern void cdvdReloadElfInfo(std::string elfoverride = std::string());
extern u32 cdvdGetElfCRC(const std::string& path);
extern s32 cdvdCtrlTrayOpen();
extern s32 cdvdCtrlTrayClose();

View File

@ -363,7 +363,8 @@ void CommonHost::OnVMResumed()
UpdateInhibitScreensaver(EmuConfig.InhibitScreensaver);
}
void CommonHost::OnGameChanged(const std::string& disc_path, const std::string& game_serial, const std::string& game_name, u32 game_crc)
void CommonHost::OnGameChanged(const std::string& disc_path, const std::string& elf_override, const std::string& game_serial,
const std::string& game_name, u32 game_crc)
{
UpdateSessionTime(game_serial);

View File

@ -71,7 +71,8 @@ namespace CommonHost
void OnVMResumed();
/// Called when the running executable changes.
void OnGameChanged(const std::string& disc_path, const std::string& game_serial, const std::string& game_name, u32 game_crc);
void OnGameChanged(const std::string& disc_path, const std::string& elf_override, const std::string& game_serial,
const std::string& game_name, u32 game_crc);
/// Provided by the host; called once per frame at guest vsync.
void CPUThreadVSync();

View File

@ -252,6 +252,7 @@ namespace FullscreenUI
// Landing
//////////////////////////////////////////////////////////////////////////
static void SwitchToLanding();
static ImGuiFullscreen::FileSelectorFilters GetOpenFileFilters();
static ImGuiFullscreen::FileSelectorFilters GetDiscImageFilters();
static void DoStartPath(
const std::string& path, std::optional<s32> state_index = std::nullopt, std::optional<bool> fast_boot = std::nullopt);
@ -877,11 +878,16 @@ void FullscreenUI::DestroyResources()
// Utility
//////////////////////////////////////////////////////////////////////////
ImGuiFullscreen::FileSelectorFilters FullscreenUI::GetDiscImageFilters()
ImGuiFullscreen::FileSelectorFilters FullscreenUI::GetOpenFileFilters()
{
return {"*.bin", "*.iso", "*.cue", "*.chd", "*.cso", "*.gz", "*.elf", "*.irx", "*.gs", "*.gs.xz", "*.gs.zst", "*.dump"};
}
ImGuiFullscreen::FileSelectorFilters FullscreenUI::GetDiscImageFilters()
{
return {"*.bin", "*.iso", "*.cue", "*.chd", "*.cso", "*.gz"};
}
void FullscreenUI::DoStartPath(const std::string& path, std::optional<s32> state_index, std::optional<bool> fast_boot)
{
VMBootParameters params;
@ -914,7 +920,7 @@ void FullscreenUI::DoStartFile()
CloseFileSelector();
};
OpenFileSelector(ICON_FA_FOLDER_OPEN " Select Disc Image", false, std::move(callback), GetDiscImageFilters());
OpenFileSelector(ICON_FA_FOLDER_OPEN " Select Disc Image", false, std::move(callback), GetOpenFileFilters());
}
void FullscreenUI::DoStartBIOS()
@ -2129,16 +2135,10 @@ void FullscreenUI::SwitchToGameSettings()
auto lock = GameList::GetLock();
const GameList::Entry* entry = GameList::GetEntryForPath(s_current_game_path.c_str());
if (!entry)
{
entry = GameList::GetEntryBySerialAndCRC(s_current_game_serial.c_str(), s_current_game_crc);
if (!entry)
{
SwitchToGameSettings(s_current_game_serial.c_str(), s_current_game_crc);
return;
}
}
SwitchToGameSettings(entry);
if (entry)
SwitchToGameSettings(entry);
}
void FullscreenUI::SwitchToGameSettings(const std::string& path)
@ -2151,7 +2151,7 @@ void FullscreenUI::SwitchToGameSettings(const std::string& path)
void FullscreenUI::SwitchToGameSettings(const GameList::Entry* entry)
{
SwitchToGameSettings(entry->serial.c_str(), entry->crc);
SwitchToGameSettings((entry->type != GameList::EntryType::ELF) ? std::string_view(entry->serial) : std::string_view(), entry->crc);
s_game_settings_entry = std::make_unique<GameList::Entry>(*entry);
}
@ -2367,6 +2367,8 @@ void FullscreenUI::DrawSettingsWindow()
void FullscreenUI::DrawSummarySettingsPage()
{
SettingsInterface* bsi = GetEditingSettingsInterface();
BeginMenuButtons();
MenuHeading("Details");
@ -2379,7 +2381,7 @@ void FullscreenUI::DrawSummarySettingsPage()
CopyTextToClipboard("Game serial copied to clipboard.", s_game_settings_entry->serial);
if (MenuButton(ICON_FA_CODE " CRC", fmt::format("{:08X}", s_game_settings_entry->crc).c_str(), true))
CopyTextToClipboard("Game CRC copied to clipboard.", fmt::format("{:08X}", s_game_settings_entry->crc));
if (MenuButton(ICON_FA_COMPACT_DISC " Type", GameList::EntryTypeToString(s_game_settings_entry->type), true))
if (MenuButton(ICON_FA_LIST " Type", GameList::EntryTypeToString(s_game_settings_entry->type), true))
CopyTextToClipboard("Game type copied to clipboard.", GameList::EntryTypeToString(s_game_settings_entry->type));
if (MenuButton(ICON_FA_BOX " Region", GameList::RegionToString(s_game_settings_entry->region), true))
CopyTextToClipboard("Game region copied to clipboard.", GameList::RegionToString(s_game_settings_entry->region));
@ -2391,6 +2393,44 @@ void FullscreenUI::DrawSummarySettingsPage()
}
if (MenuButton(ICON_FA_FOLDER_OPEN " Path", s_game_settings_entry->path.c_str(), true))
CopyTextToClipboard("Game path copied to clipboard.", s_game_settings_entry->path);
if (s_game_settings_entry->type == GameList::EntryType::ELF)
{
const std::string iso_path(bsi->GetStringValue("EmuCore", "DiscPath"));
if (MenuButton(ICON_FA_COMPACT_DISC " Disc Path", iso_path.empty() ? "No Disc" : iso_path.c_str()))
{
auto callback = [](const std::string& path) {
if (!path.empty())
{
{
auto lock = Host::GetSettingsLock();
if (s_game_settings_interface)
{
s_game_settings_interface->SetStringValue("EmuCore", "DiscPath", path.c_str());
s_game_settings_interface->Save();
}
}
if (s_game_settings_entry)
{
// re-scan the entry to update its serial.
if (GameList::RescanPath(s_game_settings_entry->path))
{
auto lock = GameList::GetLock();
const GameList::Entry* entry = GameList::GetEntryForPath(s_game_settings_entry->path.c_str());
if (entry)
*s_game_settings_entry = *entry;
}
}
}
QueueResetFocus();
CloseFileSelector();
};
OpenFileSelector(ICON_FA_COMPACT_DISC " Select Disc Path", false, std::move(callback), GetDiscImageFilters());
}
}
}
else
{

View File

@ -69,6 +69,8 @@ namespace GameList
static Entry* GetMutableEntryForPath(const char* path);
static bool GetIsoSerialAndCRC(const std::string& path, s32* disc_type, std::string* serial, u32* crc);
static Region ParseDatabaseRegion(const std::string_view& db_region);
static bool GetElfListEntry(const std::string& path, GameList::Entry* entry);
static bool GetIsoListEntry(const std::string& path, GameList::Entry* entry);
@ -85,6 +87,7 @@ namespace GameList
static bool WriteEntryToCache(const GameList::Entry* entry);
static void CloseCacheFileStream();
static void DeleteCacheFile();
static void RewriteCacheFile();
static std::string GetPlayedTimeFile();
static bool ParsePlayedTimeLine(char* line, std::string& serial, PlayedTimeEntry& entry);
@ -152,8 +155,8 @@ void GameList::FillBootParametersForEntry(VMBootParameters* params, const Entry*
}
else if (entry->type == GameList::EntryType::ELF)
{
params->filename.clear();
params->source_type = CDVD_SourceType::NoDisc;
params->filename = VMManager::GetDiscOverrideFromGameSettings(entry->path);
params->source_type = params->filename.empty() ? CDVD_SourceType::NoDisc : CDVD_SourceType::Iso;
params->elf_override = entry->path;
}
else
@ -164,6 +167,29 @@ void GameList::FillBootParametersForEntry(VMBootParameters* params, const Entry*
}
}
bool GameList::GetIsoSerialAndCRC(const std::string& path, s32* disc_type, std::string* serial, u32* crc)
{
// This isn't great, we really want to make it all thread-local...
CDVD = &CDVDapi_Iso;
if (CDVD->open(path.c_str()) != 0)
return false;
*disc_type = DoCDVDdetectDiskType();
cdvdReloadElfInfo();
*serial = DiscSerial;
*crc = ElfCRC;
DoCDVDclose();
// TODO(Stenzek): These globals are **awful**. Clean it up.
DiscSerial.clear();
ElfCRC = 0;
ElfEntry = -1;
LastELF.clear();
return true;
}
bool GameList::GetElfListEntry(const std::string& path, GameList::Entry* entry)
{
const s64 file_size = FileSystem::GetPathFileSize(path.c_str());
@ -181,17 +207,101 @@ bool GameList::GetElfListEntry(const std::string& path, GameList::Entry* entry)
return false;
}
const std::string display_name(FileSystem::GetDisplayNameFromPath(path));
entry->path = path;
entry->serial.clear();
entry->title = Path::GetFileTitle(display_name);
entry->title = Path::GetFileTitle(FileSystem::GetDisplayNameFromPath(path));
entry->region = Region::Other;
entry->total_size = static_cast<u64>(file_size);
entry->type = EntryType::ELF;
entry->compatibility_rating = CompatibilityRating::Unknown;
std::string disc_path(VMManager::GetDiscOverrideFromGameSettings(path));
if (!disc_path.empty())
{
s32 disc_type;
u32 disc_crc;
if (GetIsoSerialAndCRC(disc_path, &disc_type, &entry->serial, &disc_crc))
{
// use serial/region/compat info from the db
if (const GameDatabaseSchema::GameEntry* db_entry = GameDatabase::findGame(entry->serial))
{
entry->compatibility_rating = db_entry->compat;
entry->region = ParseDatabaseRegion(db_entry->region);
}
}
}
return true;
}
// clang-format off
GameList::Region GameList::ParseDatabaseRegion(const std::string_view& db_region)
{
// clang-format off
////// NTSC //////
//////////////////
if (StringUtil::StartsWith(db_region, "NTSC-B"))
return Region::NTSC_B;
else if (StringUtil::StartsWith(db_region, "NTSC-C"))
return Region::NTSC_C;
else if (StringUtil::StartsWith(db_region, "NTSC-HK"))
return Region::NTSC_HK;
else if (StringUtil::StartsWith(db_region, "NTSC-J"))
return Region::NTSC_J;
else if (StringUtil::StartsWith(db_region, "NTSC-K"))
return Region::NTSC_K;
else if (StringUtil::StartsWith(db_region, "NTSC-T"))
return Region::NTSC_T;
else if (StringUtil::StartsWith(db_region, "NTSC-U"))
return Region::NTSC_U;
////// PAL //////
//////////////////
else if (StringUtil::StartsWith(db_region, "PAL-AF"))
return Region::PAL_AF;
else if (StringUtil::StartsWith(db_region, "PAL-AU"))
return Region::PAL_AU;
else if (StringUtil::StartsWith(db_region, "PAL-A"))
return Region::PAL_A;
else if (StringUtil::StartsWith(db_region, "PAL-BE"))
return Region::PAL_BE;
else if (StringUtil::StartsWith(db_region, "PAL-E"))
return Region::PAL_E;
else if (StringUtil::StartsWith(db_region, "PAL-FI"))
return Region::PAL_FI;
else if (StringUtil::StartsWith(db_region, "PAL-F"))
return Region::PAL_F;
else if (StringUtil::StartsWith(db_region, "PAL-GR"))
return Region::PAL_GR;
else if (StringUtil::StartsWith(db_region, "PAL-G"))
return Region::PAL_G;
else if (StringUtil::StartsWith(db_region, "PAL-IN"))
return Region::PAL_IN;
else if (StringUtil::StartsWith(db_region, "PAL-I"))
return Region::PAL_I;
else if (StringUtil::StartsWith(db_region, "PAL-M"))
return Region::PAL_M;
else if (StringUtil::StartsWith(db_region, "PAL-NL"))
return Region::PAL_NL;
else if (StringUtil::StartsWith(db_region, "PAL-NO"))
return Region::PAL_NO;
else if (StringUtil::StartsWith(db_region, "PAL-P"))
return Region::PAL_P;
else if (StringUtil::StartsWith(db_region, "PAL-R"))
return Region::PAL_R;
else if (StringUtil::StartsWith(db_region, "PAL-SC"))
return Region::PAL_SC;
else if (StringUtil::StartsWith(db_region, "PAL-SWI"))
return Region::PAL_SWI;
else if (StringUtil::StartsWith(db_region, "PAL-SW"))
return Region::PAL_SW;
else if (StringUtil::StartsWith(db_region, "PAL-S"))
return Region::PAL_S;
else if (StringUtil::StartsWith(db_region, "PAL-UK"))
return Region::PAL_UK;
else
return Region::Other;
// clang-format on
}
bool GameList::GetIsoListEntry(const std::string& path, GameList::Entry* entry)
{
FILESYSTEM_STAT_DATA sd;
@ -203,8 +313,11 @@ bool GameList::GetIsoListEntry(const std::string& path, GameList::Entry* entry)
if (CDVD->open(path.c_str()) != 0)
return false;
const s32 type = DoCDVDdetectDiskType();
switch (type)
s32 disc_type;
if (!GetIsoSerialAndCRC(path, &disc_type, &entry->serial, &entry->crc))
return false;
switch (disc_type)
{
case CDVD_TYPE_PSCD:
case CDVD_TYPE_PSCDDA:
@ -219,92 +332,18 @@ bool GameList::GetIsoListEntry(const std::string& path, GameList::Entry* entry)
case CDVD_TYPE_ILLEGAL:
default:
DoCDVDclose();
return false;
}
cdvdReloadElfInfo();
entry->path = path;
entry->serial = DiscSerial;
entry->crc = ElfCRC;
entry->total_size = sd.Size;
entry->compatibility_rating = CompatibilityRating::Unknown;
DoCDVDclose();
// TODO(Stenzek): These globals are **awful**. Clean it up.
DiscSerial.clear();
ElfCRC = 0;
ElfEntry = -1;
LastELF.clear();
if (const GameDatabaseSchema::GameEntry* db_entry = GameDatabase::findGame(entry->serial))
{
entry->title = std::move(db_entry->name);
entry->compatibility_rating = db_entry->compat;
////// NTSC //////
//////////////////
if (StringUtil::StartsWith(db_entry->region, "NTSC-B"))
entry->region = Region::NTSC_B;
else if (StringUtil::StartsWith(db_entry->region, "NTSC-C"))
entry->region = Region::NTSC_C;
else if (StringUtil::StartsWith(db_entry->region, "NTSC-HK"))
entry->region = Region::NTSC_HK;
else if (StringUtil::StartsWith(db_entry->region, "NTSC-J"))
entry->region = Region::NTSC_J;
else if (StringUtil::StartsWith(db_entry->region, "NTSC-K"))
entry->region = Region::NTSC_K;
else if (StringUtil::StartsWith(db_entry->region, "NTSC-T"))
entry->region = Region::NTSC_T;
else if (StringUtil::StartsWith(db_entry->region, "NTSC-U"))
entry->region = Region::NTSC_U;
////// PAL //////
//////////////////
else if (StringUtil::StartsWith(db_entry->region, "PAL-AF"))
entry->region = Region::PAL_AF;
else if (StringUtil::StartsWith(db_entry->region, "PAL-AU"))
entry->region = Region::PAL_AU;
else if (StringUtil::StartsWith(db_entry->region, "PAL-A"))
entry->region = Region::PAL_A;
else if (StringUtil::StartsWith(db_entry->region, "PAL-BE"))
entry->region = Region::PAL_BE;
else if (StringUtil::StartsWith(db_entry->region, "PAL-E"))
entry->region = Region::PAL_E;
else if (StringUtil::StartsWith(db_entry->region, "PAL-FI"))
entry->region = Region::PAL_FI;
else if (StringUtil::StartsWith(db_entry->region, "PAL-F"))
entry->region = Region::PAL_F;
else if (StringUtil::StartsWith(db_entry->region, "PAL-GR"))
entry->region = Region::PAL_GR;
else if (StringUtil::StartsWith(db_entry->region, "PAL-G"))
entry->region = Region::PAL_G;
else if (StringUtil::StartsWith(db_entry->region, "PAL-IN"))
entry->region = Region::PAL_IN;
else if (StringUtil::StartsWith(db_entry->region, "PAL-I"))
entry->region = Region::PAL_I;
else if (StringUtil::StartsWith(db_entry->region, "PAL-M"))
entry->region = Region::PAL_M;
else if (StringUtil::StartsWith(db_entry->region, "PAL-NL"))
entry->region = Region::PAL_NL;
else if (StringUtil::StartsWith(db_entry->region, "PAL-NO"))
entry->region = Region::PAL_NO;
else if (StringUtil::StartsWith(db_entry->region, "PAL-P"))
entry->region = Region::PAL_P;
else if (StringUtil::StartsWith(db_entry->region, "PAL-R"))
entry->region = Region::PAL_R;
else if (StringUtil::StartsWith(db_entry->region, "PAL-SC"))
entry->region = Region::PAL_SC;
else if (StringUtil::StartsWith(db_entry->region, "PAL-SWI"))
entry->region = Region::PAL_SWI;
else if (StringUtil::StartsWith(db_entry->region, "PAL-SW"))
entry->region = Region::PAL_SW;
else if (StringUtil::StartsWith(db_entry->region, "PAL-S"))
entry->region = Region::PAL_S;
else if (StringUtil::StartsWith(db_entry->region, "PAL-UK"))
entry->region = Region::PAL_UK;
else
entry->region = Region::Other;
entry->region = ParseDatabaseRegion(db_entry->region);
}
else
{
@ -314,7 +353,7 @@ bool GameList::GetIsoListEntry(const std::string& path, GameList::Entry* entry)
return true;
}
// clang-format off
bool GameList::PopulateEntryFromPath(const std::string& path, GameList::Entry* entry)
{
if (VMManager::IsElfFileName(path.c_str()))
@ -365,8 +404,7 @@ static bool ReadU64(std::FILE* stream, u64* dest)
static bool WriteString(std::FILE* stream, const std::string& str)
{
const u32 size = static_cast<u32>(str.size());
return (std::fwrite(&size, sizeof(size), 1, stream) > 0 &&
(size == 0 || std::fwrite(str.data(), size, 1, stream) > 0));
return (std::fwrite(&size, sizeof(size), 1, stream) > 0 && (size == 0 || std::fwrite(str.data(), size, 1, stream) > 0));
}
static bool WriteU8(std::FILE* stream, u8 dest)
@ -388,10 +426,10 @@ bool GameList::LoadEntriesFromCache(std::FILE* stream)
{
u32 file_signature, file_version;
s64 start_pos, file_size;
if (!ReadU32(stream, &file_signature) || !ReadU32(stream, &file_version) ||
file_signature != GAME_LIST_CACHE_SIGNATURE || file_version != GAME_LIST_CACHE_VERSION ||
(start_pos = FileSystem::FTell64(stream)) < 0 || FileSystem::FSeek64(stream, 0, SEEK_END) != 0 ||
(file_size = FileSystem::FTell64(stream)) < 0 || FileSystem::FSeek64(stream, start_pos, SEEK_SET) != 0)
if (!ReadU32(stream, &file_signature) || !ReadU32(stream, &file_version) || file_signature != GAME_LIST_CACHE_SIGNATURE ||
file_version != GAME_LIST_CACHE_VERSION || (start_pos = FileSystem::FTell64(stream)) < 0 ||
FileSystem::FSeek64(stream, 0, SEEK_END) != 0 || (file_size = FileSystem::FTell64(stream)) < 0 ||
FileSystem::FSeek64(stream, start_pos, SEEK_SET) != 0)
{
Console.Warning("Game list cache is corrupted");
return false;
@ -407,11 +445,10 @@ bool GameList::LoadEntriesFromCache(std::FILE* stream)
u8 compatibility_rating;
u64 last_modified_time;
if (!ReadString(stream, &path) || !ReadString(stream, &ge.serial) || !ReadString(stream, &ge.title) ||
!ReadU8(stream, &type) || !ReadU8(stream, &region) || !ReadU64(stream, &ge.total_size) ||
!ReadU64(stream, &last_modified_time) || !ReadU32(stream, &ge.crc) || !ReadU8(stream, &compatibility_rating) ||
region >= static_cast<u8>(Region::Count) || type >= static_cast<u8>(EntryType::Count) ||
compatibility_rating > static_cast<u8>(CompatibilityRating::Perfect))
if (!ReadString(stream, &path) || !ReadString(stream, &ge.serial) || !ReadString(stream, &ge.title) || !ReadU8(stream, &type) ||
!ReadU8(stream, &region) || !ReadU64(stream, &ge.total_size) || !ReadU64(stream, &last_modified_time) ||
!ReadU32(stream, &ge.crc) || !ReadU8(stream, &compatibility_rating) || region >= static_cast<u8>(Region::Count) ||
type >= static_cast<u8>(EntryType::Count) || compatibility_rating > static_cast<u8>(CompatibilityRating::Perfect))
{
Console.Warning("Game list cache entry is corrupted");
return false;
@ -485,8 +522,7 @@ bool GameList::OpenCacheForWriting()
// new cache file, write header
if (!WriteU32(s_cache_write_stream, GAME_LIST_CACHE_SIGNATURE) ||
!WriteU32(s_cache_write_stream, GAME_LIST_CACHE_VERSION))
if (!WriteU32(s_cache_write_stream, GAME_LIST_CACHE_SIGNATURE) || !WriteU32(s_cache_write_stream, GAME_LIST_CACHE_VERSION))
{
Console.Error("Failed to write game list cache header");
std::fclose(s_cache_write_stream);
@ -541,13 +577,27 @@ void GameList::DeleteCacheFile()
Console.Warning("Failed to delete game list cache '%s'", cache_filename.c_str());
}
void GameList::RewriteCacheFile()
{
CloseCacheFileStream();
DeleteCacheFile();
if (OpenCacheForWriting())
{
for (const GameList::Entry& entry : s_entries)
WriteEntryToCache(&entry);
CloseCacheFileStream();
}
}
static bool IsPathExcluded(const std::vector<std::string>& excluded_paths, const std::string& path)
{
return (std::find(excluded_paths.begin(), excluded_paths.end(), path) != excluded_paths.end());
}
void GameList::ScanDirectory(const char* path, bool recursive, bool only_cache, const std::vector<std::string>& excluded_paths, const PlayedTimeMap& played_time_map,
ProgressCallback* progress)
void GameList::ScanDirectory(const char* path, bool recursive, bool only_cache, const std::vector<std::string>& excluded_paths,
const PlayedTimeMap& played_time_map, ProgressCallback* progress)
{
Console.WriteLn("Scanning %s%s", path, recursive ? " (recursively)" : "");
@ -568,15 +618,13 @@ void GameList::ScanDirectory(const char* path, bool recursive, bool only_cache,
{
files_scanned++;
if (progress->IsCancelled() || !GameList::IsScannableFilename(ffd.FileName) ||
IsPathExcluded(excluded_paths, ffd.FileName))
if (progress->IsCancelled() || !GameList::IsScannableFilename(ffd.FileName) || IsPathExcluded(excluded_paths, ffd.FileName))
{
continue;
}
std::unique_lock lock(s_mutex);
if (GetEntryForPath(ffd.FileName.c_str()) ||
AddFileFromCache(ffd.FileName, ffd.ModificationTime, played_time_map) || only_cache)
if (GetEntryForPath(ffd.FileName.c_str()) || AddFileFromCache(ffd.FileName, ffd.ModificationTime, played_time_map) || only_cache)
{
continue;
}
@ -607,8 +655,8 @@ bool GameList::AddFileFromCache(const std::string& path, std::time_t timestamp,
return true;
}
bool GameList::ScanFile(std::string path, std::time_t timestamp, std::unique_lock<std::recursive_mutex>& lock,
const PlayedTimeMap& played_time_map)
bool GameList::ScanFile(
std::string path, std::time_t timestamp, std::unique_lock<std::recursive_mutex>& lock, const PlayedTimeMap& played_time_map)
{
// don't block UI while scanning
lock.unlock();
@ -636,6 +684,13 @@ bool GameList::ScanFile(std::string path, std::time_t timestamp, std::unique_loc
}
lock.lock();
// remove if present
auto it = std::find_if(
s_entries.begin(), s_entries.end(), [&entry](const Entry& existing_entry) { return (existing_entry.path == entry.path); });
if (it != s_entries.end())
s_entries.erase(it);
s_entries.push_back(std::move(entry));
return true;
}
@ -753,6 +808,32 @@ void GameList::Refresh(bool invalidate_cache, bool only_cache, ProgressCallback*
s_cache_map.clear();
}
bool GameList::RescanPath(const std::string& path)
{
FILESYSTEM_STAT_DATA sd;
if (!FileSystem::StatFile(path.c_str(), &sd))
return false;
std::unique_lock lock(s_mutex);
const PlayedTimeMap played_time(LoadPlayedTimeMap(GetPlayedTimeFile()));
{
// cancel if excluded
const std::vector<std::string> excluded_paths(Host::GetBaseStringListSetting("GameList", "ExcludedPaths"));
if (std::find(excluded_paths.begin(), excluded_paths.end(), path) != excluded_paths.end())
return false;
}
// re-scan!
if (!ScanFile(path, sd.ModificationTime, lock, played_time))
return true;
// update cache.. this is far from ideal, but since everything's variable length, all we can do.
RewriteCacheFile();
return true;
}
std::string GameList::GetPlayedTimeFile()
{
return Path::Combine(EmuFolders::Settings, "playtime.dat");
@ -770,8 +851,8 @@ bool GameList::ParsePlayedTimeLine(char* line, std::string& serial, PlayedTimeEn
const std::string_view serial_tok(StringUtil::StripWhitespace(std::string_view(line, PLAYED_TIME_SERIAL_LENGTH)));
const std::string_view total_played_time_tok(
StringUtil::StripWhitespace(std::string_view(line + PLAYED_TIME_SERIAL_LENGTH + 1, PLAYED_TIME_LAST_TIME_LENGTH)));
const std::string_view last_played_time_tok(StringUtil::StripWhitespace(std::string_view(
line + PLAYED_TIME_SERIAL_LENGTH + 1 + PLAYED_TIME_LAST_TIME_LENGTH + 1, PLAYED_TIME_TOTAL_TIME_LENGTH)));
const std::string_view last_played_time_tok(StringUtil::StripWhitespace(
std::string_view(line + PLAYED_TIME_SERIAL_LENGTH + 1 + PLAYED_TIME_LAST_TIME_LENGTH + 1, PLAYED_TIME_TOTAL_TIME_LENGTH)));
const std::optional<u64> total_played_time(StringUtil::FromChars<u64>(total_played_time_tok));
const std::optional<u64> last_played_time(StringUtil::FromChars<u64>(last_played_time_tok));
@ -789,9 +870,8 @@ bool GameList::ParsePlayedTimeLine(char* line, std::string& serial, PlayedTimeEn
std::string GameList::MakePlayedTimeLine(const std::string& serial, const PlayedTimeEntry& entry)
{
return fmt::format("{:<{}} {:<{}} {:<{}}\n", serial, static_cast<unsigned>(PLAYED_TIME_SERIAL_LENGTH),
entry.total_played_time, static_cast<unsigned>(PLAYED_TIME_TOTAL_TIME_LENGTH),
entry.last_played_time, static_cast<unsigned>(PLAYED_TIME_LAST_TIME_LENGTH));
return fmt::format("{:<{}} {:<{}} {:<{}}\n", serial, static_cast<unsigned>(PLAYED_TIME_SERIAL_LENGTH), entry.total_played_time,
static_cast<unsigned>(PLAYED_TIME_TOTAL_TIME_LENGTH), entry.last_played_time, static_cast<unsigned>(PLAYED_TIME_LAST_TIME_LENGTH));
}
GameList::PlayedTimeMap GameList::LoadPlayedTimeMap(const std::string& path)
@ -837,10 +917,10 @@ GameList::PlayedTimeMap GameList::LoadPlayedTimeMap(const std::string& path)
return ret;
}
GameList::PlayedTimeEntry GameList::UpdatePlayedTimeFile(const std::string& path, const std::string& serial,
std::time_t last_time, std::time_t add_time)
GameList::PlayedTimeEntry GameList::UpdatePlayedTimeFile(
const std::string& path, const std::string& serial, std::time_t last_time, std::time_t add_time)
{
const PlayedTimeEntry new_entry{ last_time, add_time };
const PlayedTimeEntry new_entry{last_time, add_time};
auto fp = FileSystem::OpenManagedCFile(path.c_str(), "r+b");
@ -887,8 +967,7 @@ GameList::PlayedTimeEntry GameList::UpdatePlayedTimeFile(const std::string& path
line_entry.total_played_time = (last_time != 0) ? (line_entry.total_played_time + add_time) : 0;
std::string new_line(MakePlayedTimeLine(serial, line_entry));
if (FileSystem::FSeek64(fp.get(), line_pos, SEEK_SET) != 0 ||
std::fwrite(new_line.data(), new_line.length(), 1, fp.get()) != 1 ||
if (FileSystem::FSeek64(fp.get(), line_pos, SEEK_SET) != 0 || std::fwrite(new_line.data(), new_line.length(), 1, fp.get()) != 1 ||
std::fflush(fp.get()) != 0)
{
Console.Error("Failed to update '%s'.", path.c_str());
@ -901,8 +980,7 @@ GameList::PlayedTimeEntry GameList::UpdatePlayedTimeFile(const std::string& path
{
// new entry.
std::string new_line(MakePlayedTimeLine(serial, new_entry));
if (FileSystem::FSeek64(fp.get(), 0, SEEK_END) != 0 ||
std::fwrite(new_line.data(), new_line.length(), 1, fp.get()) != 1)
if (FileSystem::FSeek64(fp.get(), 0, SEEK_END) != 0 || std::fwrite(new_line.data(), new_line.length(), 1, fp.get()) != 1)
{
Console.Error("Failed to write '%s'.", path.c_str());
}
@ -992,7 +1070,7 @@ std::string GameList::FormatTimestamp(std::time_t timestamp)
ret = "Today";
}
else if ((ctime.tm_year == ttime.tm_year && ctime.tm_yday == (ttime.tm_yday + 1)) ||
(ctime.tm_yday == 0 && (ctime.tm_year - 1) == ttime.tm_year))
(ctime.tm_yday == 0 && (ctime.tm_year - 1) == ttime.tm_year))
{
ret = "Yesterday";
}
@ -1194,38 +1272,39 @@ bool GameList::DownloadCovers(const std::vector<std::string>& url_templates, boo
// we could actually do a few in parallel here...
std::string filename(Common::HTTPDownloader::URLDecode(url));
downloader->CreateRequest(std::move(url), [use_serial, &save_callback, entry_path = std::move(entry_path),
filename = std::move(filename)](s32 status_code, const std::string& content_type, Common::HTTPDownloader::Request::Data data) {
if (status_code != Common::HTTPDownloader::HTTP_OK || data.empty())
return;
downloader->CreateRequest(
std::move(url), [use_serial, &save_callback, entry_path = std::move(entry_path), filename = std::move(filename)](
s32 status_code, const std::string& content_type, Common::HTTPDownloader::Request::Data data) {
if (status_code != Common::HTTPDownloader::HTTP_OK || data.empty())
return;
std::unique_lock lock(s_mutex);
const GameList::Entry* entry = GetEntryForPath(entry_path.c_str());
if (!entry || !GetCoverImagePathForEntry(entry).empty())
return;
std::unique_lock lock(s_mutex);
const GameList::Entry* entry = GetEntryForPath(entry_path.c_str());
if (!entry || !GetCoverImagePathForEntry(entry).empty())
return;
// prefer the content type from the response for the extension
// otherwise, if it's missing, and the request didn't have an extension.. fall back to jpegs.
std::string template_filename;
std::string content_type_extension(Common::HTTPDownloader::GetExtensionForContentType(content_type));
// prefer the content type from the response for the extension
// otherwise, if it's missing, and the request didn't have an extension.. fall back to jpegs.
std::string template_filename;
std::string content_type_extension(Common::HTTPDownloader::GetExtensionForContentType(content_type));
// don't treat the domain name as an extension..
const std::string::size_type last_slash = filename.find('/');
const std::string::size_type last_dot = filename.find('.');
if (!content_type_extension.empty())
template_filename = fmt::format("cover.{}", content_type_extension);
else if (last_slash != std::string::npos && last_dot != std::string::npos && last_dot > last_slash)
template_filename = Path::GetFileName(filename);
else
template_filename = "cover.jpg";
// don't treat the domain name as an extension..
const std::string::size_type last_slash = filename.find('/');
const std::string::size_type last_dot = filename.find('.');
if (!content_type_extension.empty())
template_filename = fmt::format("cover.{}", content_type_extension);
else if (last_slash != std::string::npos && last_dot != std::string::npos && last_dot > last_slash)
template_filename = Path::GetFileName(filename);
else
template_filename = "cover.jpg";
std::string write_path(GetNewCoverImagePathForEntry(entry, template_filename.c_str(), use_serial));
if (write_path.empty())
return;
std::string write_path(GetNewCoverImagePathForEntry(entry, template_filename.c_str(), use_serial));
if (write_path.empty())
return;
if (FileSystem::WriteBinaryFile(write_path.c_str(), data.data(), data.size()) && save_callback)
save_callback(entry, std::move(write_path));
});
if (FileSystem::WriteBinaryFile(write_path.c_str(), data.data(), data.size()) && save_callback)
save_callback(entry, std::move(write_path));
});
downloader->WaitForAllRequests();
progress->IncrementProgressValue();
}

View File

@ -122,6 +122,9 @@ namespace GameList
/// If only_cache is set, no new files will be scanned, only those present in the cache.
void Refresh(bool invalidate_cache, bool only_cache = false, ProgressCallback* progress = nullptr);
/// Re-scans a single entry in the game list.
bool RescanPath(const std::string& path);
/// Add played time for the specified serial.
void AddPlayedTimeForSerial(const std::string& serial, std::time_t last_time, std::time_t add_time);
void ClearPlayedTimeForSerial(const std::string& serial);

View File

@ -372,6 +372,23 @@ std::string VMManager::GetGameSettingsPath(const std::string_view& game_serial,
Path::Combine(EmuFolders::GameSettings, fmt::format("{}_{:08X}.ini", sanitized_serial, game_crc));
}
std::string VMManager::GetDiscOverrideFromGameSettings(const std::string& elf_path)
{
std::string iso_path;
if (const u32 crc = cdvdGetElfCRC(elf_path); crc != 0)
{
INISettingsInterface si(GetGameSettingsPath(std::string_view(), crc));
if (si.Load())
{
iso_path = si.GetStringValue("EmuCore", "DiscPath");
if (!iso_path.empty())
Console.WriteLn(fmt::format("Disc override for ELF at '{}' is '{}'", elf_path, iso_path));
}
}
return iso_path;
}
std::string VMManager::GetInputProfilePath(const std::string_view& name)
{
return Path::Combine(EmuFolders::InputProfiles, fmt::format("{}.ini", name));
@ -423,12 +440,20 @@ void VMManager::RequestDisplaySize(float scale /*= 0.0f*/)
Host::RequestResizeHostDisplay(iwidth, iheight);
}
std::string VMManager::GetSerialForGameSettings()
{
// If we're running an ELF, we don't want to use the serial for any ISO override
// for game settings, since the game settings is where we define the override.
std::unique_lock lock(s_info_mutex);
return s_elf_override.empty() ? std::string(s_game_serial) : std::string();
}
bool VMManager::UpdateGameSettingsLayer()
{
std::unique_ptr<INISettingsInterface> new_interface;
if (s_game_crc != 0 && Host::GetBaseBoolSettingValue("EmuCore", "EnablePerGameSettings", true))
{
std::string filename(GetGameSettingsPath(s_game_serial.c_str(), s_game_crc));
std::string filename(GetGameSettingsPath(GetSerialForGameSettings(), s_game_crc));
if (!FileSystem::FileExists(filename.c_str()))
{
// try the legacy format (crc.ini)
@ -669,7 +694,11 @@ void VMManager::UpdateRunningGame(bool resetting, bool game_starting)
if (const GameDatabaseSchema::GameEntry* game = GameDatabase::findGame(s_game_serial))
{
s_game_name = game->name;
if (!s_elf_override.empty())
s_game_name = Path::GetFileTitle(FileSystem::GetDisplayNameFromPath(s_elf_override));
else
s_game_name = game->name;
memcardFilters = game->memcardFiltersAsString();
}
else
@ -706,7 +735,7 @@ void VMManager::UpdateRunningGame(bool resetting, bool game_starting)
GetMTGS().SendGameCRC(new_crc);
Host::OnGameChanged(s_disc_path, s_game_serial, s_game_name, s_game_crc);
Host::OnGameChanged(s_disc_path, s_elf_override, s_game_serial, s_game_name, s_game_crc);
#if 0
// TODO: Enable this when the debugger is added to Qt, and it's active. Otherwise, this is just a waste of time.
@ -745,8 +774,20 @@ bool VMManager::AutoDetectSource(const std::string& filename)
}
else if (IsElfFileName(display_name))
{
// alternative way of booting an elf, change the elf override, and use no disc.
CDVDsys_ChangeSource(CDVD_SourceType::NoDisc);
// alternative way of booting an elf, change the elf override, and (optionally) use the disc
// specified in the game settings.
std::string disc_path(GetDiscOverrideFromGameSettings(filename));
if (!disc_path.empty())
{
CDVDsys_SetFile(CDVD_SourceType::Iso, disc_path);
CDVDsys_ChangeSource(CDVD_SourceType::Iso);
s_disc_path = std::move(disc_path);
}
else
{
CDVDsys_ChangeSource(CDVD_SourceType::NoDisc);
}
s_elf_override = filename;
return true;
}
@ -1050,11 +1091,12 @@ void VMManager::Shutdown(bool save_resume_state)
std::unique_lock lock(s_info_mutex);
s_disc_path.clear();
s_elf_override.clear();
s_game_crc = 0;
s_patches_crc = 0;
s_game_serial.clear();
s_game_name.clear();
Host::OnGameChanged(s_disc_path, s_game_serial, s_game_name, 0);
Host::OnGameChanged(s_disc_path, s_elf_override, s_game_serial, s_game_name, 0);
}
s_active_game_fixes = 0;
s_active_widescreen_patches = 0;

View File

@ -164,9 +164,15 @@ namespace VMManager
/// Returns true if the specified path is a disc/elf/etc.
bool IsLoadableFileName(const std::string_view& path);
/// Returns the serial to use when computing the game settings path for the current game.
std::string GetSerialForGameSettings();
/// Returns the path for the game settings ini file for the specified CRC.
std::string GetGameSettingsPath(const std::string_view& game_serial, u32 game_crc);
/// Returns the ISO override for an ELF via gamesettings.
std::string GetDiscOverrideFromGameSettings(const std::string& elf_path);
/// Returns the path for the input profile ini file with the specified name (may not exist).
std::string GetInputProfilePath(const std::string_view& name);
@ -201,7 +207,7 @@ namespace VMManager
void EntryPointCompilingOnCPUThread();
void GameStartingOnCPUThread();
void VSyncOnCPUThread();
}
} // namespace Internal
} // namespace VMManager
@ -246,11 +252,12 @@ namespace Host
void OnSaveStateSaved(const std::string_view& filename);
/// Provided by the host; called when the running executable changes.
void OnGameChanged(const std::string& disc_path, const std::string& game_serial, const std::string& game_name, u32 game_crc);
void OnGameChanged(const std::string& disc_path, const std::string& elf_override, const std::string& game_serial,
const std::string& game_name, u32 game_crc);
/// Provided by the host; called once per frame at guest vsync.
void CPUThreadVSync();
/// Provided by the host; called when a state is saved, and the frontend should invalidate its save state cache.
void InvalidateSaveStateCache();
}
} // namespace Host