mirror of https://github.com/PCSX2/pcsx2.git
Qt: Add disc path option for ELF game list entries
This commit is contained in:
parent
8ac0bd452e
commit
9da8e9280f
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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, ®ion) || !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, ®ion) || !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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue