System: Allow separate configuration for multi-disc games

This commit is contained in:
Stenzek 2024-12-16 01:22:48 +10:00
parent 23c221be01
commit b634eecd21
No known key found for this signature in database
11 changed files with 228 additions and 89 deletions

View File

@ -441,6 +441,7 @@ struct ALIGN_TO_CACHE_LINE UIState
// Settings
SettingsPage settings_page = SettingsPage::Interface;
std::unique_ptr<INISettingsInterface> game_settings_interface;
const GameDatabase::Entry* game_settings_db_entry;
std::unique_ptr<GameList::Entry> game_settings_entry;
std::vector<std::pair<std::string, bool>> game_list_directories_cache;
GPUDevice::AdapterInfoList graphics_adapter_list_cache;
@ -870,8 +871,8 @@ void FullscreenUI::Render()
if (s_state.game_settings_interface->IsEmpty())
{
if (FileSystem::FileExists(s_state.game_settings_interface->GetFileName().c_str()) &&
!FileSystem::DeleteFile(s_state.game_settings_interface->GetFileName().c_str(), &error))
if (FileSystem::FileExists(s_state.game_settings_interface->GetPath().c_str()) &&
!FileSystem::DeleteFile(s_state.game_settings_interface->GetPath().c_str(), &error))
{
ImGuiFullscreen::OpenInfoMessageDialog(
FSUI_STR("Error"), fmt::format(FSUI_FSTR("An error occurred while deleting empty game settings:\n{}"),
@ -2724,6 +2725,7 @@ void FullscreenUI::SwitchToSettings()
{
s_state.game_settings_entry.reset();
s_state.game_settings_interface.reset();
s_state.game_settings_db_entry = nullptr;
s_state.game_patch_list = {};
s_state.enabled_game_patch_cache = {};
s_state.game_cheats_list = {};
@ -2740,8 +2742,9 @@ void FullscreenUI::SwitchToSettings()
void FullscreenUI::SwitchToGameSettingsForSerial(std::string_view serial)
{
s_state.game_settings_entry.reset();
s_state.game_settings_interface = std::make_unique<INISettingsInterface>(System::GetGameSettingsPath(serial));
s_state.game_settings_interface->Load();
s_state.game_settings_db_entry = GameDatabase::GetEntryForSerial(serial);
s_state.game_settings_interface =
System::GetGameSettingsInterface(s_state.game_settings_db_entry, serial, true, false);
PopulatePatchesAndCheatsList(serial);
s_state.current_main_window = MainWindowType::Settings;
s_state.settings_page = SettingsPage::Summary;
@ -2828,7 +2831,7 @@ void FullscreenUI::DoCopyGameSettings()
SetSettingsChanged(s_state.game_settings_interface.get());
ShowToast("Game Settings Copied", fmt::format(FSUI_FSTR("Game settings initialized with global settings for '{}'."),
Path::GetFileTitle(s_state.game_settings_interface->GetFileName())));
Path::GetFileTitle(s_state.game_settings_interface->GetPath())));
}
void FullscreenUI::DoClearGameSettings()
@ -2837,13 +2840,13 @@ void FullscreenUI::DoClearGameSettings()
return;
s_state.game_settings_interface->Clear();
if (!s_state.game_settings_interface->GetFileName().empty())
FileSystem::DeleteFile(s_state.game_settings_interface->GetFileName().c_str());
if (!s_state.game_settings_interface->GetPath().empty())
FileSystem::DeleteFile(s_state.game_settings_interface->GetPath().c_str());
SetSettingsChanged(s_state.game_settings_interface.get());
ShowToast("Game Settings Cleared", fmt::format(FSUI_FSTR("Game settings have been cleared for '{}'."),
Path::GetFileTitle(s_state.game_settings_interface->GetFileName())));
Path::GetFileTitle(s_state.game_settings_interface->GetPath())));
}
void FullscreenUI::DrawSettingsWindow()
@ -3098,6 +3101,18 @@ void FullscreenUI::DrawSummarySettingsPage()
MenuHeading(FSUI_CSTR("Options"));
if (s_state.game_settings_db_entry && !s_state.game_settings_db_entry->disc_set_serials.empty())
{
// only enable for first disc
const bool is_first_disc =
(s_state.game_settings_db_entry->serial == s_state.game_settings_db_entry->disc_set_serials.front());
DrawToggleSetting(
GetEditingSettingsInterface(), FSUI_ICONSTR(ICON_FA_COMPACT_DISC, "Use Separate Disc Settings"),
FSUI_CSTR(
"Uses separate game settings for each disc of multi-disc games. Can only be set on the first/main disc."),
"Main", "UseSeparateConfigForDiscSet", !is_first_disc, is_first_disc, false);
}
if (MenuButton(FSUI_ICONSTR(ICON_FA_COPY, "Copy Settings"),
FSUI_CSTR("Copies the current global settings to this game.")))
{

View File

@ -1171,20 +1171,6 @@ DiscRegion System::GetRegionForPsf(const char* path)
return psf.GetRegion();
}
std::string System::GetGameSettingsPath(std::string_view game_serial)
{
// multi-disc games => always use the first disc
const GameDatabase::Entry* entry = GameDatabase::GetEntryForSerial(game_serial);
const std::string_view serial_for_path =
(entry && !entry->disc_set_serials.empty()) ? entry->disc_set_serials.front() : game_serial;
return Path::Combine(EmuFolders::GameSettings, fmt::format("{}.ini", Path::SanitizeFileName(serial_for_path)));
}
std::string System::GetInputProfilePath(std::string_view name)
{
return Path::Combine(EmuFolders::InputProfiles, fmt::format("{}.ini", name));
}
bool System::RecreateGPU(GPURenderer renderer, bool force_recreate_device, bool update_display /* = true*/)
{
ClearMemorySaveStates();
@ -1487,27 +1473,108 @@ void System::ReloadInputProfile(bool display_osd_messages)
ApplySettings(display_osd_messages);
}
bool System::UpdateGameSettingsLayer()
std::string System::GetInputProfilePath(std::string_view name)
{
std::unique_ptr<INISettingsInterface> new_interface;
if (g_settings.apply_game_settings && !s_state.running_game_serial.empty())
return Path::Combine(EmuFolders::InputProfiles, fmt::format("{}.ini", name));
}
std::string System::GetGameSettingsPath(std::string_view game_serial, bool ignore_disc_set)
{
// multi-disc games => always use the first disc
const GameDatabase::Entry* entry = ignore_disc_set ? nullptr : GameDatabase::GetEntryForSerial(game_serial);
const std::string_view serial_for_path =
(entry && !entry->disc_set_serials.empty()) ? entry->disc_set_serials.front() : game_serial;
return Path::Combine(EmuFolders::GameSettings, fmt::format("{}.ini", Path::SanitizeFileName(serial_for_path)));
}
std::unique_ptr<INISettingsInterface> System::GetGameSettingsInterface(const GameDatabase::Entry* dbentry,
std::string_view serial, bool create, bool quiet)
{
std::unique_ptr<INISettingsInterface> ret;
std::string path = GetGameSettingsPath(serial, false);
if (FileSystem::FileExists(path.c_str()))
{
std::string filename(GetGameSettingsPath(s_state.running_game_serial));
if (FileSystem::FileExists(filename.c_str()))
if (!quiet)
INFO_COLOR_LOG(StrongCyan, "Loading game settings from '{}'...", Path::GetFileName(path));
Error error;
ret = std::make_unique<INISettingsInterface>(std::move(path));
if (ret->Load(&error))
{
INFO_LOG("Loading game settings from '{}'...", Path::GetFileName(filename));
new_interface = std::make_unique<INISettingsInterface>(std::move(filename));
if (!new_interface->Load())
// Check for separate disc configuration.
if (dbentry && !dbentry->disc_set_serials.empty() && dbentry->disc_set_serials.front() != serial)
{
ERROR_LOG("Failed to parse game settings ini '{}'", new_interface->GetFileName());
new_interface.reset();
if (ret->GetBoolValue("Main", "UseSeparateConfigForDiscSet", false))
{
if (!quiet)
{
INFO_COLOR_LOG(StrongCyan, "Using separate disc game settings serial {} for disc set {}", serial,
dbentry->disc_set_serials.front());
}
// Load the disc specific ini.
path = GetGameSettingsPath(serial, true);
if (FileSystem::FileExists(path.c_str()))
{
if (!ret->Load(std::move(path), &error))
{
if (!quiet)
{
ERROR_LOG("Failed to parse separate disc game settings ini '{}': {}", Path::GetFileName(ret->GetPath()),
error.GetDescription());
}
if (create)
ret->Clear();
else
ret.reset();
}
}
else
{
if (!quiet)
INFO_COLOR_LOG(StrongCyan, "No separate disc game settings found (tried '{}')", Path::GetFileName(path));
ret.reset();
// return empty ini struct?
if (create)
ret = std::make_unique<INISettingsInterface>(std::move(path));
}
}
}
}
else
{
INFO_LOG("No game settings found (tried '{}')", Path::GetFileName(filename));
if (!quiet)
{
ERROR_LOG("Failed to parse game settings ini '{}': {}", Path::GetFileName(ret->GetPath()),
error.GetDescription());
}
if (!create)
ret.reset();
}
}
else
{
if (!quiet)
INFO_COLOR_LOG(StrongCyan, "No game settings found (tried '{}')", Path::GetFileName(path));
// return empty ini struct?
if (create)
ret = std::make_unique<INISettingsInterface>(std::move(path));
}
return ret;
}
bool System::UpdateGameSettingsLayer()
{
std::unique_ptr<INISettingsInterface> new_interface;
if (g_settings.apply_game_settings && !s_state.running_game_serial.empty())
new_interface = GetGameSettingsInterface(s_state.running_game_entry, s_state.running_game_serial, false, false);
std::string input_profile_name;
if (new_interface)
@ -1543,7 +1610,7 @@ void System::UpdateInputSettingsLayer(std::string input_profile_name, std::uniqu
input_interface = std::make_unique<INISettingsInterface>(std::move(filename));
if (!input_interface->Load())
{
ERROR_LOG("Failed to parse input profile ini '{}'", Path::GetFileName(input_interface->GetFileName()));
ERROR_LOG("Failed to parse input profile ini '{}'", Path::GetFileName(input_interface->GetPath()));
input_interface.reset();
input_profile_name = {};
}
@ -5543,20 +5610,12 @@ std::string System::GetGameMemoryCardPath(std::string_view serial, std::string_v
std::unique_ptr<INISettingsInterface> ini;
if (!serial.empty())
{
std::string game_settings_path = GetGameSettingsPath(serial);
if (FileSystem::FileExists(game_settings_path.c_str()))
ini = GetGameSettingsInterface(GameDatabase::GetEntryForSerial(serial), serial, false, true);
if (ini && ini->ContainsValue(section, type_key))
{
ini = std::make_unique<INISettingsInterface>(std::move(game_settings_path));
if (!ini->Load())
{
ini.reset();
}
else if (ini->ContainsValue(section, type_key))
{
type = Settings::ParseMemoryCardTypeName(
ini->GetTinyStringValue(section, type_key, Settings::GetMemoryCardTypeName(global_type)))
.value_or(global_type);
}
type = Settings::ParseMemoryCardTypeName(
ini->GetTinyStringValue(section, type_key, Settings::GetMemoryCardTypeName(global_type)))
.value_or(global_type);
}
}
else if (type == MemoryCardType::PerGame)

View File

@ -28,6 +28,7 @@ enum class GPUVSyncMode : u8;
class Controller;
class GPUTexture;
class INISettingsInterface;
class MediaCapture;
namespace BIOS {
@ -153,7 +154,12 @@ DiscRegion GetRegionForExe(const char* path);
DiscRegion GetRegionForPsf(const char* path);
/// Returns the path for the game settings ini file for the specified serial.
std::string GetGameSettingsPath(std::string_view game_serial);
std::string GetGameSettingsPath(std::string_view game_serial, bool ignore_disc_set);
/// Returns the loaded interface for the game settings ini file for the specified serial. If create is true, an empty
/// ini reader will be returned if the file does not exist. If quit is true, no log messages will be emitted.
std::unique_ptr<INISettingsInterface> GetGameSettingsInterface(const GameDatabase::Entry* dbentry,
std::string_view serial, bool create, bool quiet);
/// Returns the path for the input profile ini file with the specified name (may not exist).
std::string GetInputProfilePath(std::string_view name);

View File

@ -221,7 +221,7 @@ void ControllerSettingsWindow::onNewProfileClicked()
{
QMessageBox::critical(
this, tr("Error"),
tr("Failed to save the new profile to '%1'.").arg(QString::fromStdString(temp_si.GetFileName())));
tr("Failed to save the new profile to '%1'.").arg(QString::fromStdString(temp_si.GetPath())));
return;
}

View File

@ -86,6 +86,13 @@ GameSummaryWidget::~GameSummaryWidget() = default;
void GameSummaryWidget::reloadGameSettings()
{
if (m_ui.separateDiscSettings->isVisible() && m_ui.separateDiscSettings->isEnabled())
{
m_ui.separateDiscSettings->setCheckState(
m_dialog->getBoolValue("Main", "UseSeparateConfigForDiscSet", std::nullopt).value_or(false) ? Qt::Checked :
Qt::Unchecked);
}
if (m_dialog->getBoolValue("ControllerPorts", "UseGameSettingsForController", std::nullopt).value_or(false))
{
const QSignalBlocker sb(m_ui.inputProfile);
@ -214,6 +221,28 @@ void GameSummaryWidget::populateUi(const std::string& path, const std::string& s
m_ui.entryType->setCurrentIndex(static_cast<int>(gentry->type));
}
if (entry && !entry->disc_set_serials.empty())
{
if (serial == entry->disc_set_serials.front())
{
m_ui.separateDiscSettings->setCheckState(
m_dialog->getBoolValue("Main", "UseSeparateConfigForDiscSet", std::nullopt).value_or(false) ? Qt::Checked :
Qt::Unchecked);
connect(m_ui.separateDiscSettings, &QCheckBox::checkStateChanged, this,
&GameSummaryWidget::onSeparateDiscSettingsChanged);
}
else
{
// set disabled+checked if not first disc
m_ui.separateDiscSettings->setCheckState(Qt::Checked);
m_ui.separateDiscSettings->setEnabled(false);
}
}
else
{
m_ui.separateDiscSettings->setVisible(false);
}
m_ui.compatibilityComments->setVisible(!m_compatibility_comments.isEmpty());
m_ui.inputProfile->addItem(QIcon::fromTheme(QStringLiteral("global-line")), tr("Use Global Settings"));
@ -228,6 +257,19 @@ void GameSummaryWidget::populateUi(const std::string& path, const std::string& s
updateWindowTitle();
}
void GameSummaryWidget::onSeparateDiscSettingsChanged(Qt::CheckState state)
{
if (state == Qt::Checked)
m_dialog->setBoolSettingValue("Main", "UseSeparateConfigForDiscSet", true);
else
m_dialog->removeSettingValue("Main", "UseSeparateConfigForDiscSet");
}
void GameSummaryWidget::updateWindowTitle()
{
m_dialog->setGameTitle(m_ui.title->text().toStdString());
}
void GameSummaryWidget::populateCustomAttributes()
{
auto lock = GameList::GetLock();
@ -257,11 +299,6 @@ void GameSummaryWidget::populateCustomAttributes()
}
}
void GameSummaryWidget::updateWindowTitle()
{
m_dialog->setGameTitle(m_ui.title->text().toStdString());
}
void GameSummaryWidget::setCustomTitle(const std::string& text)
{
m_ui.restoreTitle->setEnabled(!text.empty());

View File

@ -31,6 +31,7 @@ protected:
void showEvent(QShowEvent* event) override;
private Q_SLOTS:
void onSeparateDiscSettingsChanged(Qt::CheckState state);
void onCustomLanguageChanged(int language);
void onCompatibilityCommentsClicked();
void onInputProfileChanged(int index);

View File

@ -208,13 +208,6 @@
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QComboBox" name="entryType">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="8" column="1">
<layout class="QHBoxLayout" name="horizontalLayout" stretch="1,0">
<item>
@ -344,6 +337,27 @@
</item>
</layout>
</item>
<item row="5" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_4" stretch="1,0">
<item>
<widget class="QComboBox" name="entryType">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="separateDiscSettings">
<property name="toolTip">
<string>Uses separate game settings for each disc of multi-disc games. Can only be set on the first/main disc.</string>
</property>
<property name="text">
<string>Use Separate Disc Settings</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>

View File

@ -227,14 +227,14 @@ bool QtHost::SaveGameSettings(SettingsInterface* sif, bool delete_if_empty)
// if there's no keys, just toss the whole thing out
if (delete_if_empty && ini->IsEmpty())
{
INFO_LOG("Removing empty gamesettings ini {}", Path::GetFileName(ini->GetFileName()));
INFO_LOG("Removing empty gamesettings ini {}", Path::GetFileName(ini->GetPath()));
// grab the settings lock while we're writing the file, that way the CPU thread doesn't try
// to read it at the same time.
const auto lock = Host::GetSettingsLock();
if (FileSystem::FileExists(ini->GetFileName().c_str()) &&
!FileSystem::DeleteFile(ini->GetFileName().c_str(), &error))
if (FileSystem::FileExists(ini->GetPath().c_str()) &&
!FileSystem::DeleteFile(ini->GetPath().c_str(), &error))
{
Host::ReportErrorAsync(
TRANSLATE_SV("QtHost", "Error"),
@ -472,7 +472,7 @@ bool QtHost::InitializeConfig(std::string settings_filename)
QStringLiteral(
"Failed to save configuration to\n\n%1\n\nThe error was: %2\n\nPlease ensure this directory is writable. You "
"can also try portable mode by creating portable.txt in the same directory you installed DuckStation into.")
.arg(QString::fromStdString(s_base_settings_interface->GetFileName()))
.arg(QString::fromStdString(s_base_settings_interface->GetPath()))
.arg(QString::fromStdString(error.GetDescription())));
return false;
}

View File

@ -662,7 +662,10 @@ void SettingsWindow::setGameTitle(std::string title)
{
m_title = std::move(title);
const QString window_title = tr("%1 [%2]").arg(QString::fromStdString(m_title)).arg(QString::fromStdString(m_serial));
const QString window_title =
tr("%1 [%2]")
.arg(QString::fromStdString(m_title))
.arg(QtUtils::StringViewToQString(m_sif ? Path::GetFileName(m_sif->GetPath()) : std::string_view(m_serial)));
setWindowTitle(window_title);
}
@ -695,13 +698,13 @@ SettingsWindow* SettingsWindow::openGamePropertiesDialog(const std::string& path
}
std::string real_serial = dentry ? std::string(dentry->serial) : std::move(serial);
std::string ini_filename = System::GetGameSettingsPath(real_serial);
std::unique_ptr<INISettingsInterface> sif = System::GetGameSettingsInterface(dentry, real_serial, true, false);
// check for an existing dialog with this crc
// check for an existing dialog with this serial
for (SettingsWindow* dialog : s_open_game_properties_dialogs)
{
if (dialog->isPerGameSettings() &&
static_cast<INISettingsInterface*>(dialog->getSettingsInterface())->GetFileName() == ini_filename)
static_cast<INISettingsInterface*>(dialog->getSettingsInterface())->GetPath() == sif->GetPath())
{
dialog->show();
dialog->raise();
@ -713,10 +716,6 @@ SettingsWindow* SettingsWindow::openGamePropertiesDialog(const std::string& path
}
}
std::unique_ptr<INISettingsInterface> sif = std::make_unique<INISettingsInterface>(std::move(ini_filename));
if (FileSystem::FileExists(sif->GetFileName().c_str()))
sif->Load();
SettingsWindow* dialog =
new SettingsWindow(path, std::move(title), std::move(real_serial), hash, region, dentry, std::move(sif));
dialog->show();
@ -737,14 +736,14 @@ void SettingsWindow::closeGamePropertiesDialogs()
bool SettingsWindow::setGameSettingsBoolForSerial(const std::string& serial, const char* section, const char* key,
bool value)
{
std::string ini_filename = System::GetGameSettingsPath(serial);
if (ini_filename.empty())
if (serial.empty())
return false;
INISettingsInterface sif(std::move(ini_filename));
if (FileSystem::FileExists(sif.GetFileName().c_str()))
sif.Load();
std::unique_ptr<INISettingsInterface> sif =
System::GetGameSettingsInterface(GameDatabase::GetEntryForSerial(serial), serial, true, false);
if (!sif)
return false;
sif.SetBoolValue(section, key, value);
return sif.Save();
sif->SetBoolValue(section, key, value);
return sif->Save();
}

View File

@ -19,7 +19,7 @@ LOG_CHANNEL(Settings);
// we only allow one ini to be parsed at any point in time.
static std::mutex s_ini_load_save_mutex;
INISettingsInterface::INISettingsInterface(std::string filename) : m_filename(std::move(filename)), m_ini(true, true)
INISettingsInterface::INISettingsInterface(std::string filename) : m_path(std::move(filename)), m_ini(true, true)
{
}
@ -31,7 +31,7 @@ INISettingsInterface::~INISettingsInterface()
bool INISettingsInterface::Load(Error* error /* = nullptr */)
{
if (m_filename.empty())
if (m_path.empty())
{
Error::SetStringView(error, "Filename is not set.");
return false;
@ -39,7 +39,7 @@ bool INISettingsInterface::Load(Error* error /* = nullptr */)
std::unique_lock lock(s_ini_load_save_mutex);
SI_Error err = SI_FAIL;
auto fp = FileSystem::OpenManagedCFile(m_filename.c_str(), "rb", error);
auto fp = FileSystem::OpenManagedCFile(m_path.c_str(), "rb", error);
if (fp)
{
err = m_ini.LoadFile(fp.get());
@ -50,16 +50,23 @@ bool INISettingsInterface::Load(Error* error /* = nullptr */)
return (err == SI_OK);
}
bool INISettingsInterface::Load(std::string new_path, Error* error /*= nullptr*/)
{
m_path = std::move(new_path);
m_ini.Reset();
return Load(error);
}
bool INISettingsInterface::Save(Error* error /* = nullptr */)
{
if (m_filename.empty())
if (m_path.empty())
{
Error::SetStringView(error, "Filename is not set.");
return false;
}
std::unique_lock lock(s_ini_load_save_mutex);
FileSystem::AtomicRenamedFile fp = FileSystem::CreateAtomicRenamedFile(m_filename, error);
FileSystem::AtomicRenamedFile fp = FileSystem::CreateAtomicRenamedFile(m_path, error);
SI_Error err = SI_FAIL;
if (fp)
{
@ -78,7 +85,7 @@ bool INISettingsInterface::Save(Error* error /* = nullptr */)
if (err != SI_OK)
{
WARNING_LOG("Failed to save settings to '{}'.", m_filename);
WARNING_LOG("Failed to save settings to '{}'.", m_path);
return false;
}

View File

@ -13,12 +13,13 @@
class INISettingsInterface final : public SettingsInterface
{
public:
INISettingsInterface(std::string filename);
INISettingsInterface(std::string path);
~INISettingsInterface() override;
const std::string& GetFileName() const { return m_filename; }
const std::string& GetPath() const { return m_path; }
bool Load(Error* error = nullptr);
bool Load(std::string new_path, Error* error = nullptr);
bool Save(Error* error = nullptr) override;
void Clear() override;
@ -61,7 +62,7 @@ public:
using SettingsInterface::GetUIntValue;
private:
std::string m_filename;
std::string m_path;
CSimpleIniA m_ini;
bool m_dirty = false;
};