Qt: Support populating game list entry at runtime

Re-enables support for modifying cheats when running direct from disc.
This commit is contained in:
Stenzek
2025-11-30 14:16:11 +10:00
parent d8a92f7c97
commit d612f8eea8
12 changed files with 139 additions and 80 deletions

View File

@@ -1613,7 +1613,22 @@ bool FullscreenUI::SwitchToGameSettingsForPath(const std::string& path, Settings
const GameList::Entry* entry = !path.empty() ? GameList::GetEntryForPath(path) : nullptr;
if (!entry || entry->serial.empty())
{
ShowToast(OSDMessageType::Info, {}, FSUI_STR("Game properties is only available for scanned games."));
Host::RunOnCPUThread([page]() {
Error error;
GameList::Entry dynamic_entry;
if (System::PopulateGameListEntryFromCurrentGame(&dynamic_entry, &error))
{
GPUThread::RunOnThread([dynamic_entry = std::move(dynamic_entry), page]() {
if (IsInitialized())
SwitchToGameSettings(&dynamic_entry, page);
});
}
else
{
ShowToast(OSDMessageType::Info, {}, error.TakeDescription());
}
});
return false;
}
@@ -1992,6 +2007,13 @@ void FullscreenUI::DrawSummarySettingsPage(bool show_localized_titles)
BeginMenuButtons();
ResetFocusHere();
if (!s_settings_locals.game_settings_entry || s_settings_locals.game_settings_entry->is_runtime_populated)
{
MenuButton(FSUI_ICONVSTR(ICON_EMOJI_WARNING,
"This game was not scanned by DuckStation. Some functionality is not available."),
{}, false);
}
MenuHeading(FSUI_VSTR("Details"));
if (s_settings_locals.game_settings_entry)
@@ -2034,10 +2056,6 @@ void FullscreenUI::DrawSummarySettingsPage(bool show_localized_titles)
CopyTextToClipboard(FSUI_STR("Game path copied to clipboard."), s_settings_locals.game_settings_entry->path);
}
}
else
{
MenuButton(FSUI_ICONVSTR(ICON_FA_BAN, "Details unavailable for game not scanned in game list."), "");
}
MenuHeading(FSUI_VSTR("Options"));

View File

@@ -205,6 +205,7 @@ TRANSLATE_NOOP("FullscreenUI", "Copies the global controller configuration to th
TRANSLATE_NOOP("FullscreenUI", "Copy Global Settings");
TRANSLATE_NOOP("FullscreenUI", "Copy Settings");
TRANSLATE_NOOP("FullscreenUI", "Could not find any CD/DVD-ROM devices. Please ensure you have a drive connected and sufficient permissions to access it.");
TRANSLATE_NOOP("FullscreenUI", "Cover Download Error");
TRANSLATE_NOOP("FullscreenUI", "Cover Downloader");
TRANSLATE_NOOP("FullscreenUI", "Cover Settings");
TRANSLATE_NOOP("FullscreenUI", "Cover set.");
@@ -233,7 +234,6 @@ TRANSLATE_NOOP("FullscreenUI", "Depth Test Transparent Polygons");
TRANSLATE_NOOP("FullscreenUI", "Desktop Mode");
TRANSLATE_NOOP("FullscreenUI", "Destination Alpha Blending");
TRANSLATE_NOOP("FullscreenUI", "Details");
TRANSLATE_NOOP("FullscreenUI", "Details unavailable for game not scanned in game list.");
TRANSLATE_NOOP("FullscreenUI", "Determines how large the on-screen messages and monitor are.");
TRANSLATE_NOOP("FullscreenUI", "Determines how long action confirmation messages are displayed on screen.");
TRANSLATE_NOOP("FullscreenUI", "Determines how long error messages are displayed on screen.");
@@ -380,7 +380,6 @@ TRANSLATE_NOOP("FullscreenUI", "Game Quick Save");
TRANSLATE_NOOP("FullscreenUI", "Game Slot {0}##game_slot_{0}");
TRANSLATE_NOOP("FullscreenUI", "Game compatibility rating copied to clipboard.");
TRANSLATE_NOOP("FullscreenUI", "Game path copied to clipboard.");
TRANSLATE_NOOP("FullscreenUI", "Game properties is only available for scanned games.");
TRANSLATE_NOOP("FullscreenUI", "Game region copied to clipboard.");
TRANSLATE_NOOP("FullscreenUI", "Game serial copied to clipboard.");
TRANSLATE_NOOP("FullscreenUI", "Game settings have been cleared for '{}'.");
@@ -754,6 +753,7 @@ TRANSLATE_NOOP("FullscreenUI", "The SDL input source supports most controllers."
TRANSLATE_NOOP("FullscreenUI", "The audio backend determines how frames produced by the emulator are submitted to the host.");
TRANSLATE_NOOP("FullscreenUI", "The selected memory card image will be used in shared mode for this slot.");
TRANSLATE_NOOP("FullscreenUI", "Theme");
TRANSLATE_NOOP("FullscreenUI", "This game was not scanned by DuckStation. Some functionality is not available.");
TRANSLATE_NOOP("FullscreenUI", "Threaded Rendering");
TRANSLATE_NOOP("FullscreenUI", "Time Played");
TRANSLATE_NOOP("FullscreenUI", "Time Played: ");

View File

@@ -401,6 +401,7 @@ void GameList::MakeInvalidEntry(Entry* entry)
entry->disc_set_member = false;
entry->has_custom_title = false;
entry->has_custom_region = false;
entry->is_runtime_populated = false;
entry->custom_language = GameDatabase::Language::MaxCount;
entry->path = {};
entry->serial = {};

View File

@@ -38,6 +38,7 @@ struct Entry
bool disc_set_member = false;
bool has_custom_title = false;
bool has_custom_region = false;
bool is_runtime_populated = false;
GameDatabase::Language custom_language = GameDatabase::Language::MaxCount;
std::string path;
@@ -96,8 +97,7 @@ const char* GetEntryTypeDisplayName(EntryType type);
bool IsScannableFilename(std::string_view path);
/// Populates a game list entry struct with information from the iso/elf.
/// Do *not* call while the system is running, it will mess with CDVD state.
/// Populates a game list entry struct with information from the specified path.
bool PopulateEntryFromPath(const std::string& path, Entry* entry);
// Game list access. It's the caller's responsibility to hold the lock while manipulating the entry in any way.

View File

@@ -679,11 +679,6 @@ ConsoleRegion System::GetRegion()
return s_state.region;
}
DiscRegion System::GetDiscRegion()
{
return CDROM::GetDiscRegion();
}
bool System::IsPALRegion()
{
return s_state.region == ConsoleRegion::PAL;
@@ -4091,19 +4086,6 @@ bool System::DumpSPURAM(std::string path, Error* error)
return FileSystem::WriteAtomicRenamedFile(std::move(path), SPU::GetRAM(), error);
}
bool System::HasMedia()
{
return CDROM::HasMedia();
}
std::string System::GetMediaPath()
{
if (!CDROM::HasMedia())
return {};
return CDROM::GetMediaPath();
}
bool System::InsertMedia(const char* path)
{
if (IsGPUDumpPath(path)) [[unlikely]]
@@ -4267,6 +4249,40 @@ void System::UpdateRunningGame(const std::string& path, CDImage* image, bool boo
s_state.running_game_hash);
}
bool System::PopulateGameListEntryFromCurrentGame(GameList::Entry* entry, Error* error)
{
if (!IsValid() || IsReplayingGPUDump() || IsPsfPath(s_state.running_game_path))
{
Error::SetStringView(error, TRANSLATE_SV("System", "No valid game is running."));
return false;
}
entry->path = s_state.running_game_path;
entry->serial = s_state.running_game_serial;
entry->title = s_state.running_game_title;
entry->dbentry = s_state.running_game_entry;
entry->hash = s_state.running_game_hash;
entry->has_custom_title = s_state.running_game_custom_title;
if (CDROM::HasMedia())
{
entry->type = CDROM::GetMedia()->HasSubImages() ? GameList::EntryType::Playlist : GameList::EntryType::Disc;
entry->region = CDROM::GetDiscRegion();
}
else
{
entry->type = GameList::EntryType::PSExe;
entry->region = ((s_state.region == ConsoleRegion::NTSC_U) ?
DiscRegion::NTSC_U :
((s_state.region == ConsoleRegion::NTSC_J) ? DiscRegion::NTSC_J : DiscRegion::PAL));
}
entry->achievements_game_id = Achievements::GetGameID();
entry->is_runtime_populated = true;
return true;
}
bool System::CheckForRequiredSubQ(Error* error)
{
if (IsReplayingGPUDump() || !s_state.running_game_entry ||

View File

@@ -34,6 +34,9 @@ class MediaCapture;
namespace GameDatabase {
struct Entry;
}
namespace GameList {
struct Entry;
}
struct SystemBootParameters
{
@@ -186,7 +189,6 @@ void CancelPendingStartup();
void InterruptExecution();
ConsoleRegion GetRegion();
DiscRegion GetDiscRegion();
bool IsPALRegion();
/// Taints - flags that are set on the system and only cleared on reset.
@@ -238,6 +240,9 @@ BootMode GetBootMode();
/// Returns the time elapsed in the current play session.
u64 GetSessionPlayedTime();
/// Populates a game list entry struct with information from the currently-running game.
bool PopulateGameListEntryFromCurrentGame(GameList::Entry* entry, Error* error);
void FormatLatencyStats(SmallStringBase& str);
/// Loads global settings (i.e. EmuConfig).
@@ -323,8 +328,6 @@ bool DumpVRAM(std::string path, Error* error);
/// Dumps sound RAM to a file.
bool DumpSPURAM(std::string path, Error* error);
bool HasMedia();
std::string GetMediaPath();
bool InsertMedia(const char* path);
void RemoveMedia();

View File

@@ -28,7 +28,6 @@ GameSummaryWidget::GameSummaryWidget(const GameList::Entry* entry, SettingsWindo
: m_dialog(dialog)
{
m_ui.setupUi(this);
m_ui.revision->setVisible(false);
for (u32 i = 0; i < static_cast<u32>(GameList::EntryType::MaxCount); i++)
{
@@ -236,7 +235,10 @@ void GameSummaryWidget::populateUi(const GameList::Entry* entry)
m_ui.inputProfile->addItem(QString::fromStdString(name));
reloadGameSettings();
populateTracksInfo();
if (!entry->is_runtime_populated)
populateTracksInfo();
else
disableWidgetsForRuntimeScannedEntry();
}
void GameSummaryWidget::onSeparateDiscSettingsChanged(Qt::CheckState state)
@@ -299,21 +301,6 @@ void GameSummaryWidget::onCustomLanguageChanged(int language)
g_main_window->refreshGameListModel();
}
void GameSummaryWidget::setRevisionText(const QString& text)
{
if (text.isEmpty())
return;
if (m_ui.verifySpacer)
{
m_ui.verifyLayout->removeItem(m_ui.verifySpacer);
delete m_ui.verifySpacer;
m_ui.verifySpacer = nullptr;
}
m_ui.revision->setText(text);
m_ui.revision->setVisible(true);
}
static QString MSFToString(const CDImage::Position& position)
{
return QStringLiteral("%1:%2:%3")
@@ -334,10 +321,10 @@ void GameSummaryWidget::populateTracksInfo()
if (!image)
return;
setRevisionText(tr("%1 tracks covering %2 MB (%3 MB on disk)")
.arg(image->GetTrackCount())
.arg(((image->GetLBACount() * CDImage::RAW_SECTOR_SIZE) + 1048575) / 1048576)
.arg((image->GetSizeOnDisk() + 1048575) / 1048576));
m_ui.revision->setText(tr("%1 tracks covering %2 MB (%3 MB on disk)")
.arg(image->GetTrackCount())
.arg(((image->GetLBACount() * CDImage::RAW_SECTOR_SIZE) + 1048575) / 1048576)
.arg((image->GetSizeOnDisk() + 1048575) / 1048576));
const u32 num_tracks = image->GetTrackCount();
for (u32 track = 1; track <= num_tracks; track++)
@@ -358,6 +345,20 @@ void GameSummaryWidget::populateTracksInfo()
}
}
void GameSummaryWidget::disableWidgetsForRuntimeScannedEntry()
{
m_ui.tracks->setEnabled(false);
m_ui.computeHashes->setEnabled(false);
m_ui.title->setReadOnly(true);
m_ui.restoreTitle->setEnabled(false);
m_ui.region->setEnabled(false);
m_ui.restoreRegion->setEnabled(false);
m_ui.languages->setReadOnly(true);
m_ui.customLanguage->setEnabled(false);
m_ui.revision->setText(tr("This game was not scanned by DuckStation. Some functionality is not available."));
m_ui.tracks->setVisible(false);
}
void GameSummaryWidget::onCompatibilityCommentsClicked()
{
QDialog* const dlg = new QDialog(QtUtils::GetRootWidget(this));
@@ -587,7 +588,7 @@ void GameSummaryWidget::processHashResults(const CDImageHasher::TrackHashes& tra
}
}
setRevisionText(text);
m_ui.revision->setText(text);
// update in ui
for (size_t i = 0; i < track_hashes.size(); i++)

View File

@@ -34,9 +34,9 @@ private:
void populateUi(const GameList::Entry* entry);
void setCustomTitle(const std::string& text);
void setCustomRegion(int region);
void setRevisionText(const QString& text);
void populateTracksInfo();
void disableWidgetsForRuntimeScannedEntry();
void onSeparateDiscSettingsChanged(Qt::CheckState state);
void onCustomLanguageChanged(int language);

View File

@@ -259,20 +259,7 @@
</widget>
</item>
<item row="12" column="1" colspan="2">
<layout class="QHBoxLayout" name="verifyLayout" stretch="0,1,0">
<item>
<spacer name="verifySpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<layout class="QHBoxLayout" name="verifyLayout" stretch="1,0">
<item>
<widget class="QLineEdit" name="revision">
<property name="minimumSize">
@@ -341,6 +328,19 @@
</column>
</widget>
</item>
<item row="14" column="0" colspan="3">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>

View File

@@ -2446,7 +2446,8 @@ void MainWindow::connectSignals()
connect(m_ui.actionControllerProfiles, &QAction::triggered, this, &MainWindow::onSettingsControllerProfilesTriggered);
connect(m_ui.actionViewToolbar, &QAction::triggered, this, &MainWindow::onViewToolbarActionTriggered);
connect(m_ui.actionViewLockToolbar, &QAction::triggered, this, &MainWindow::onViewToolbarLockActionTriggered);
connect(m_ui.actionViewSmallToolbarIcons, &QAction::triggered, this, &MainWindow::onViewToolbarSmallIconsActionTriggered);
connect(m_ui.actionViewSmallToolbarIcons, &QAction::triggered, this,
&MainWindow::onViewToolbarSmallIconsActionTriggered);
connect(m_ui.actionViewToolbarLabels, &QAction::triggered, this, &MainWindow::onViewToolbarLabelsActionTriggered);
connect(m_ui.actionViewToolbarLabelsBesideIcons, &QAction::triggered, this,
&MainWindow::onViewToolbarLabelsBesideIconsActionTriggered);
@@ -2725,21 +2726,18 @@ void MainWindow::openGamePropertiesForCurrentGame(const char* category /* = null
return;
auto lock = GameList::GetLock();
const GameList::Entry* entry = GameList::GetEntryForPath(s_current_game_path.toStdString());
const std::string game_path = s_current_game_path.toStdString();
const GameList::Entry* entry = GameList::GetEntryForPath(game_path);
if (entry && entry->disc_set_member && !entry->dbentry->IsFirstDiscInSet() &&
!System::ShouldUseSeparateDiscSettingsForSerial(entry->serial))
{
// show for first disc instead
entry = GameList::GetFirstDiscSetMember(entry->dbentry->disc_set);
}
if (!entry)
{
QtUtils::AsyncMessageBox(this, QMessageBox::Critical, tr("Error"),
tr("Game properties is only available for scanned games."));
return;
}
SettingsWindow::openGamePropertiesDialog(entry, category);
if (entry)
SettingsWindow::openGamePropertiesDialog(entry, category);
else
g_emu_thread->openGamePropertiesForCurrentGame(category ? QString::fromUtf8(category) : QString());
}
ControllerSettingsWindow* MainWindow::getControllerSettingsWindow()

View File

@@ -1360,6 +1360,29 @@ void EmuThread::startControllerTest()
});
}
void EmuThread::openGamePropertiesForCurrentGame(const QString& category)
{
if (!isCurrentThread())
{
QMetaObject::invokeMethod(this, &EmuThread::openGamePropertiesForCurrentGame, Qt::QueuedConnection, category);
return;
}
Error error;
GameList::Entry dynamic_entry;
if (System::PopulateGameListEntryFromCurrentGame(&dynamic_entry, &error))
{
Host::RunOnUIThread([dynamic_entry = std::move(dynamic_entry), category]() {
SettingsWindow::openGamePropertiesDialog(&dynamic_entry,
category.isEmpty() ? nullptr : category.toUtf8().constData());
});
}
else
{
emit errorReported(tr("Error"), QString::fromStdString(error.GetDescription()));
}
}
void EmuThread::runOnEmuThread(const std::function<void()>& callback)
{
callback();
@@ -3365,10 +3388,8 @@ int main(int argc, char* argv[])
}
// When running in batch mode, ensure game list is loaded, but don't scan for any new files.
if (!autoboot)
if (!s_state.batch_mode)
g_main_window->refreshGameList(false);
else
GameList::Refresh(false, true);
// Don't bother showing the window in no-gui mode.
if (!s_state.nogui_mode)

View File

@@ -181,6 +181,7 @@ public:
void reloadTextureReplacements();
void captureGPUFrameDump();
void startControllerTest();
void openGamePropertiesForCurrentGame(const QString& category = {});
void setGPUThreadRunIdle(bool active);
void updateFullscreenUITheme();
void runOnEmuThread(const std::function<void()>& callback);