Skip to content

Commit

Permalink
System: Allow separate configuration for multi-disc games
Browse files Browse the repository at this point in the history
  • Loading branch information
stenzek committed Dec 15, 2024
1 parent 23c221b commit b634eec
Show file tree
Hide file tree
Showing 11 changed files with 228 additions and 89 deletions.
31 changes: 23 additions & 8 deletions src/core/fullscreen_ui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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{}"),
Expand Down Expand Up @@ -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 = {};
Expand All @@ -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;
Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand Down Expand Up @@ -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.")))
{
Expand Down
137 changes: 98 additions & 39 deletions src/core/system.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 = {};
}
Expand Down Expand Up @@ -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)
Expand Down
8 changes: 7 additions & 1 deletion src/core/system.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ enum class GPUVSyncMode : u8;
class Controller;

class GPUTexture;
class INISettingsInterface;
class MediaCapture;

namespace BIOS {
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/duckstation-qt/controllersettingswindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
47 changes: 42 additions & 5 deletions src/duckstation-qt/gamesummarywidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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"));
Expand All @@ -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();
Expand Down Expand Up @@ -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());
Expand Down
1 change: 1 addition & 0 deletions src/duckstation-qt/gamesummarywidget.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class GameSummaryWidget : public QWidget
void showEvent(QShowEvent* event) override;

private Q_SLOTS:
void onSeparateDiscSettingsChanged(Qt::CheckState state);
void onCustomLanguageChanged(int language);
void onCompatibilityCommentsClicked();
void onInputProfileChanged(int index);
Expand Down
Loading

0 comments on commit b634eec

Please sign in to comment.