diff --git a/README.md b/README.md
index 1d76ce271..d8d3e53a6 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,7 @@ A "BIOS" ROM image is required to to start the emulator and to play games. You c
## Latest News
+- 2020/08/20: Per-game setting overrides added. Mostly for compatibility, but some options are customizable.
- 2020/08/19: CPU PGXP mode added. It is very slow and incompatible with the recompiler, only use for games which need it.
- 2020/08/15: Playlist support/single memcard for multi-disc games in Qt frontend added.
- 2020/08/07: Automatic updater for standalone Windows builds.
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index 6b68305eb..4d2cbad5f 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -26,6 +26,8 @@ add_library(core
dma.h
game_list.cpp
game_list.h
+ game_settings.cpp
+ game_settings.h
gpu.cpp
gpu.h
gpu_commands.cpp
@@ -96,7 +98,7 @@ set(RECOMPILER_SRCS
target_include_directories(core PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/..")
target_include_directories(core PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/..")
-target_link_libraries(core PUBLIC Threads::Threads common imgui tinyxml2 zlib vulkan-loader)
+target_link_libraries(core PUBLIC Threads::Threads common imgui tinyxml2 zlib vulkan-loader simpleini)
target_link_libraries(core PRIVATE glad stb)
if(WIN32)
diff --git a/src/core/core.vcxproj b/src/core/core.vcxproj
index 07b9b2198..bc5aeadf1 100644
--- a/src/core/core.vcxproj
+++ b/src/core/core.vcxproj
@@ -60,6 +60,7 @@
+
@@ -107,6 +108,7 @@
+
@@ -142,6 +144,12 @@
+
+ {bb08260f-6fbc-46af-8924-090ee71360c6}
+
+
+ {3773f4cc-614e-4028-8595-22e08ca649e3}
+
{ed601289-ac1a-46b8-a8ed-17db9eb73423}
@@ -299,7 +307,7 @@
WITH_RECOMPILER=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)
true
ProgramDatabase
- $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
+ $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
true
false
stdcpp17
@@ -325,7 +333,7 @@
WITH_RECOMPILER=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)
true
ProgramDatabase
- $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
+ $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
true
false
stdcpp17
@@ -351,7 +359,7 @@
WITH_RECOMPILER=1;_ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)
true
ProgramDatabase
- $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
+ $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
Default
true
false
@@ -380,7 +388,7 @@
WITH_RECOMPILER=1;_ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)
true
ProgramDatabase
- $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
+ $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
Default
true
false
@@ -408,7 +416,7 @@
MaxSpeed
true
WITH_RECOMPILER=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)
- $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
+ $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
true
false
stdcpp17
@@ -435,7 +443,7 @@
MaxSpeed
true
WITH_RECOMPILER=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)
- $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
+ $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
true
true
stdcpp17
@@ -463,7 +471,7 @@
MaxSpeed
true
WITH_RECOMPILER=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)
- $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
+ $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
true
false
stdcpp17
@@ -490,7 +498,7 @@
MaxSpeed
true
WITH_RECOMPILER=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)
- $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
+ $(SolutionDir)dep\msvc\include;$(SolutionDir)dep\glad\include;$(SolutionDir)dep\stb\include;$(SolutionDir)dep\imgui\include;$(SolutionDir)dep\xbyak\xbyak;$(SolutionDir)dep\tinyxml2\include;$(SolutionDir)dep\simpleini\include;$(SolutionDir)dep\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)
true
true
stdcpp17
diff --git a/src/core/core.vcxproj.filters b/src/core/core.vcxproj.filters
index 9acf9dc28..0d86f8170 100644
--- a/src/core/core.vcxproj.filters
+++ b/src/core/core.vcxproj.filters
@@ -47,6 +47,7 @@
+
@@ -97,5 +98,6 @@
+
\ No newline at end of file
diff --git a/src/core/game_list.cpp b/src/core/game_list.cpp
index fff5e7305..6c0bd4420 100644
--- a/src/core/game_list.cpp
+++ b/src/core/game_list.cpp
@@ -446,6 +446,12 @@ bool GameList::GetGameListEntry(const std::string& path, GameListEntry* entry)
entry->compatibility_rating = compatibility_entry->compatibility_rating;
else
Log_WarningPrintf("'%s' (%s) not found in compatibility list", entry->code.c_str(), entry->title.c_str());
+
+ if (!m_game_settings_load_tried)
+ LoadGameSettings();
+ const GameSettings::Entry* settings = m_game_settings.GetEntry(entry->code);
+ if (settings)
+ entry->settings = *settings;
}
FILESYSTEM_STAT_DATA ffd;
@@ -577,6 +583,12 @@ bool GameList::LoadEntriesFromCache(ByteStream* stream)
ge.type = static_cast(type);
ge.compatibility_rating = static_cast(compatibility_rating);
+ if (!ge.settings.LoadFromStream(stream))
+ {
+ Log_WarningPrintf("Game list cache entry is corrupted (settings)");
+ return false;
+ }
+
auto iter = m_cache_map.find(ge.path);
if (iter != m_cache_map.end())
iter->second = std::move(ge);
@@ -625,6 +637,7 @@ bool GameList::WriteEntryToCache(const GameListEntry* entry, ByteStream* stream)
result &= WriteU8(stream, static_cast(entry->region));
result &= WriteU8(stream, static_cast(entry->type));
result &= WriteU8(stream, static_cast(entry->compatibility_rating));
+ result &= entry->settings.SaveToStream(stream);
return result;
}
@@ -853,6 +866,18 @@ const GameListEntry* GameList::GetEntryForPath(const char* path) const
return nullptr;
}
+GameListEntry* GameList::GetMutableEntryForPath(const char* path)
+{
+ const size_t path_length = std::strlen(path);
+ for (GameListEntry& entry : m_entries)
+ {
+ if (entry.path.size() == path_length && StringUtil::Strcasecmp(entry.path.c_str(), path) == 0)
+ return &entry;
+ }
+
+ return nullptr;
+}
+
const GameListDatabaseEntry* GameList::GetDatabaseEntryForCode(const std::string& code) const
{
if (!m_database_load_tried)
@@ -1272,3 +1297,46 @@ std::string GameList::ExportCompatibilityEntry(const GameListCompatibilityEntry*
entry_elem->Accept(&printer);
return std::string(printer.CStr(), printer.CStrSize());
}
+
+void GameList::LoadGameSettings()
+{
+ if (m_game_settings_load_tried)
+ return;
+
+ m_game_settings_load_tried = true;
+
+ if (!m_game_settings_filename.empty() && FileSystem::FileExists(m_user_game_settings_filename.c_str()))
+ m_game_settings.Load(m_game_settings_filename.c_str());
+ if (!m_user_game_settings_filename.empty() && FileSystem::FileExists(m_user_game_settings_filename.c_str()))
+ m_game_settings.Load(m_user_game_settings_filename.c_str());
+}
+
+const GameSettings::Entry* GameList::GetGameSettings(const std::string& filename, const std::string& game_code)
+{
+ const GameListEntry* entry = GetMutableEntryForPath(filename.c_str());
+ if (entry)
+ return &entry->settings;
+
+ if (!m_game_settings_load_tried)
+ LoadGameSettings();
+
+ return m_game_settings.GetEntry(game_code);
+}
+
+void GameList::UpdateGameSettings(const std::string& filename, const std::string& game_code,
+ const std::string& game_title, const GameSettings::Entry& new_entry,
+ bool save_to_list /* = true */, bool save_to_user /* = true */)
+{
+ GameListEntry* entry = GetMutableEntryForPath(filename.c_str());
+ if (entry)
+ {
+ entry->settings = new_entry;
+ RewriteCacheFile();
+ }
+
+ if (save_to_list)
+ {
+ m_game_settings.SetEntry(game_code, game_title, new_entry,
+ save_to_user ? m_user_game_settings_filename.c_str() : m_game_settings_filename.c_str());
+ }
+}
diff --git a/src/core/game_list.h b/src/core/game_list.h
index 9bbad1656..7b4f3fc80 100644
--- a/src/core/game_list.h
+++ b/src/core/game_list.h
@@ -1,4 +1,5 @@
#pragma once
+#include "game_settings.h"
#include "types.h"
#include
#include
@@ -48,6 +49,7 @@ struct GameListEntry
DiscRegion region;
GameListEntryType type;
GameListCompatibilityRating compatibility_rating;
+ GameSettings::Entry settings;
};
struct GameListCompatibilityEntry
@@ -109,6 +111,8 @@ public:
void SetCacheFilename(std::string filename) { m_cache_filename = std::move(filename); }
void SetDatabaseFilename(std::string filename) { m_database_filename = std::move(filename); }
void SetCompatibilityFilename(std::string filename) { m_compatibility_list_filename = std::move(filename); }
+ void SetGameSettingsFilename(std::string filename) { m_game_settings_filename = std::move(filename); }
+ void SetUserGameSettingsFilename(std::string filename) { m_user_game_settings_filename = std::move(filename); }
void SetSearchDirectoriesFromSettings(SettingsInterface& si);
bool IsDatabasePresent() const;
@@ -120,11 +124,15 @@ public:
static std::string ExportCompatibilityEntry(const GameListCompatibilityEntry* entry);
+ const GameSettings::Entry* GetGameSettings(const std::string& filename, const std::string& game_code);
+ void UpdateGameSettings(const std::string& filename, const std::string& game_code, const std::string& game_title,
+ const GameSettings::Entry& new_entry, bool save_to_list = true, bool save_to_user = true);
+
private:
enum : u32
{
GAME_LIST_CACHE_SIGNATURE = 0x45434C47,
- GAME_LIST_CACHE_VERSION = 5
+ GAME_LIST_CACHE_VERSION = 6
};
using DatabaseMap = std::unordered_map;
@@ -140,6 +148,8 @@ private:
class RedumpDatVisitor;
class CompatibilityListVisitor;
+ GameListEntry* GetMutableEntryForPath(const char* path);
+
static bool GetExeListEntry(const char* path, GameListEntry* entry);
bool GetM3UListEntry(const char* path, GameListEntry* entry);
@@ -163,16 +173,22 @@ private:
bool SaveCompatibilityDatabase();
bool SaveCompatibilityDatabaseForEntry(const GameListCompatibilityEntry* entry);
+ void LoadGameSettings();
+
DatabaseMap m_database;
EntryList m_entries;
CacheMap m_cache_map;
CompatibilityMap m_compatibility_list;
+ GameSettings::Database m_game_settings;
std::unique_ptr m_cache_write_stream;
std::vector m_search_directories;
std::string m_cache_filename;
std::string m_database_filename;
std::string m_compatibility_list_filename;
+ std::string m_game_settings_filename;
+ std::string m_user_game_settings_filename;
bool m_database_load_tried = false;
bool m_compatibility_list_load_tried = false;
+ bool m_game_settings_load_tried = false;
};
diff --git a/src/core/game_settings.cpp b/src/core/game_settings.cpp
new file mode 100644
index 000000000..5b0aab51d
--- /dev/null
+++ b/src/core/game_settings.cpp
@@ -0,0 +1,429 @@
+#include "game_settings.h"
+#include "common/assert.h"
+#include "common/byte_stream.h"
+#include "common/file_system.h"
+#include "common/log.h"
+#include "common/string.h"
+#include "common/string_util.h"
+#include "host_interface.h"
+#include "settings.h"
+#include
+#include
+Log_SetChannel(GameSettings);
+
+#ifdef WIN32
+#include "common/windows_headers.h"
+#endif
+#include "SimpleIni.h"
+
+namespace GameSettings {
+
+std::array, static_cast(Trait::Count)> s_trait_names = {{
+ {"ForceInterpreter", "Force Interpreter"},
+ {"ForceSoftwareRenderer", "Force Software Renderer"},
+ {"EnableInterlacing", "Enable Interlacing"},
+ {"DisableTrueColor", "Disable True Color"},
+ {"DisableUpscaling", "Disable Upscaling"},
+ {"DisableScaledDithering", "Disable Scaled Dithering"},
+ {"DisableWidescreen", "Disable Widescreen"},
+ {"DisablePGXP", "Disable PGXP"},
+ {"DisablePGXPCulling", "Disable PGXP Culling"},
+ {"EnablePGXPVertexCache", "Enable PGXP Vertex Cache"},
+ {"EnablePGXPCPUMode", "Enable PGXP CPU Mode"},
+ {"ForceDigitalController", "Force Digital Controller"},
+ {"EnableRecompilerMemoryExceptions", "Enable Recompiler Memory Exceptions"},
+}};
+
+const char* GetTraitName(Trait trait)
+{
+ DebugAssert(trait < Trait::Count);
+ return s_trait_names[static_cast(trait)].first;
+}
+
+const char* GetTraitDisplayName(Trait trait)
+{
+ DebugAssert(trait < Trait::Count);
+ return s_trait_names[static_cast(trait)].second;
+}
+
+bool Entry::HasAnySettings() const
+{
+ return traits.any() || display_active_start_offset >= 0 || display_active_end_offset > 0;
+}
+
+template
+bool ReadOptionalFromStream(ByteStream* stream, std::optional* dest)
+{
+ bool has_value;
+ if (!stream->Read2(&has_value, sizeof(has_value)))
+ return false;
+
+ if (!has_value)
+ return true;
+
+ T value;
+ if (!stream->Read2(&value, sizeof(T)))
+ return false;
+
+ *dest = value;
+ return true;
+}
+
+template
+bool WriteOptionalToStream(ByteStream* stream, const std::optional& src)
+{
+ const bool has_value = src.has_value();
+ if (!stream->Write2(&has_value, sizeof(has_value)))
+ return false;
+
+ if (!has_value)
+ return true;
+
+ return stream->Write2(&src.value(), sizeof(T));
+}
+
+bool Entry::LoadFromStream(ByteStream* stream)
+{
+ constexpr u32 num_bytes = (static_cast(Trait::Count) + 7) / 8;
+ std::array bits;
+
+ if (!stream->Read2(bits.data(), num_bytes) || !ReadOptionalFromStream(stream, &display_active_start_offset) ||
+ !ReadOptionalFromStream(stream, &display_active_end_offset) ||
+ !ReadOptionalFromStream(stream, &display_crop_mode) || !ReadOptionalFromStream(stream, &display_aspect_ratio) ||
+ !ReadOptionalFromStream(stream, &controller_1_type) || !ReadOptionalFromStream(stream, &controller_2_type))
+ {
+ return false;
+ }
+
+ traits.reset();
+ for (u32 i = 0; i < static_cast(Trait::Count); i++)
+ {
+ if ((bits[i / 8] & (1u << (i % 8))) != 0)
+ AddTrait(static_cast(i));
+ }
+
+ return true;
+}
+
+bool Entry::SaveToStream(ByteStream* stream) const
+{
+ constexpr u32 num_bytes = (static_cast(Trait::Count) + 7) / 8;
+ std::array bits;
+ bits.fill(0);
+ for (u32 i = 0; i < static_cast(Trait::Count); i++)
+ {
+ if (HasTrait(static_cast(i)))
+ bits[i / 8] |= (1u << (i % 8));
+ }
+
+ return stream->Write2(bits.data(), num_bytes) && WriteOptionalToStream(stream, display_active_start_offset) &&
+ WriteOptionalToStream(stream, display_active_end_offset) && WriteOptionalToStream(stream, display_crop_mode) &&
+ WriteOptionalToStream(stream, display_aspect_ratio) && WriteOptionalToStream(stream, controller_1_type) &&
+ WriteOptionalToStream(stream, controller_2_type);
+}
+
+static void ParseIniSection(Entry* entry, const char* section, const CSimpleIniA& ini)
+{
+ for (u32 trait = 0; trait < static_cast(Trait::Count); trait++)
+ {
+ if (ini.GetBoolValue(section, s_trait_names[trait].first, false))
+ entry->AddTrait(static_cast(trait));
+ }
+
+ long lvalue = ini.GetLongValue(section, "DisplayActiveStartOffset", 0);
+ if (lvalue != 0)
+ entry->display_active_start_offset = static_cast(lvalue);
+ lvalue = ini.GetLongValue(section, "DisplayActiveEndOffset", 0);
+ if (lvalue != 0)
+ entry->display_active_end_offset = static_cast(lvalue);
+
+ const char* cvalue = ini.GetValue(section, "DisplayCropMode", nullptr);
+ if (cvalue)
+ entry->display_crop_mode = Settings::ParseDisplayCropMode(cvalue);
+ cvalue = ini.GetValue(section, "DisplayAspectRatio", nullptr);
+ if (cvalue)
+ entry->display_aspect_ratio = Settings::ParseDisplayAspectRatio(cvalue);
+
+ cvalue = ini.GetValue(section, "Controller1Type", nullptr);
+ if (cvalue)
+ entry->controller_1_type = Settings::ParseControllerTypeName(cvalue);
+ cvalue = ini.GetValue(section, "Controller2Type", nullptr);
+ if (cvalue)
+ entry->controller_2_type = Settings::ParseControllerTypeName(cvalue);
+
+ cvalue = ini.GetValue(section, "GPUWidescreenHack", nullptr);
+ if (cvalue)
+ entry->gpu_widescreen_hack = StringUtil::FromChars(cvalue);
+}
+
+static void StoreIniSection(const Entry& entry, const char* section, CSimpleIniA& ini)
+{
+ for (u32 trait = 0; trait < static_cast(Trait::Count); trait++)
+ {
+ if (entry.HasTrait(static_cast(trait)))
+ ini.SetBoolValue(section, s_trait_names[trait].first, true);
+ }
+
+ if (entry.display_active_start_offset.has_value())
+ ini.SetLongValue(section, "DisplayActiveStartOffset", entry.display_active_start_offset.value());
+
+ if (entry.display_active_end_offset.has_value())
+ ini.SetLongValue(section, "DisplayActiveEndOffset", entry.display_active_end_offset.value());
+
+ if (entry.display_crop_mode.has_value())
+ ini.SetValue(section, "DisplayCropMode", Settings::GetDisplayCropModeName(entry.display_crop_mode.value()));
+ if (entry.display_aspect_ratio.has_value())
+ {
+ ini.SetValue(section, "DisplayAspectRatio",
+ Settings::GetDisplayAspectRatioName(entry.display_aspect_ratio.value()));
+ }
+
+ if (entry.controller_1_type.has_value())
+ ini.SetValue(section, "Controller1Type", Settings::GetControllerTypeName(entry.controller_1_type.value()));
+ if (entry.controller_2_type.has_value())
+ ini.SetValue(section, "Controller2Type", Settings::GetControllerTypeName(entry.controller_2_type.value()));
+
+ if (entry.gpu_widescreen_hack.has_value())
+ ini.SetValue(section, "GPUWidescreenHack", entry.gpu_widescreen_hack.value() ? "true" : "false");
+}
+
+Database::Database() = default;
+
+Database::~Database() = default;
+
+const GameSettings::Entry* Database::GetEntry(const std::string& code) const
+{
+ auto it = m_entries.find(code);
+ return (it != m_entries.end()) ? &it->second : nullptr;
+}
+
+bool Database::Load(const char* path)
+{
+ auto fp = FileSystem::OpenManagedCFile(path, "rb");
+ if (!fp)
+ return false;
+
+ CSimpleIniA ini;
+ SI_Error err = ini.LoadFile(fp.get());
+ if (err != S_OK)
+ {
+ Log_ErrorPrintf("Failed to parse game settings ini: %d", static_cast(err));
+ return false;
+ }
+
+ std::list sections;
+ ini.GetAllSections(sections);
+ for (const CSimpleIniA::Entry& section_entry : sections)
+ {
+ std::string code(section_entry.pItem);
+ auto it = m_entries.find(code);
+ if (it != m_entries.end())
+ {
+ ParseIniSection(&it->second, code.c_str(), ini);
+ continue;
+ }
+
+ Entry entry;
+ ParseIniSection(&entry, code.c_str(), ini);
+ m_entries.emplace(std::move(code), std::move(entry));
+ }
+
+ Log_InfoPrintf("Loaded settings for %zu games from '%s'", sections.size(), path);
+ return true;
+}
+
+void Database::SetEntry(const std::string& code, const std::string& name, const Entry& entry, const char* save_path)
+{
+ if (save_path)
+ {
+ CSimpleIniA ini;
+ if (FileSystem::FileExists(save_path))
+ {
+ auto fp = FileSystem::OpenManagedCFile(save_path, "rb");
+ if (fp)
+ {
+ SI_Error err = ini.LoadFile(fp.get());
+ if (err != S_OK)
+ Log_ErrorPrintf("Failed to parse game settings ini: %d. Contents will be lost.", static_cast(err));
+ }
+ else
+ {
+ Log_ErrorPrintf("Failed to open existing settings ini: '%s'", save_path);
+ }
+ }
+
+ ini.Delete(code.c_str(), nullptr, false);
+ ini.SetValue(code.c_str(), nullptr, nullptr, SmallString::FromFormat("# %s (%s)", code.c_str(), name.c_str()),
+ false);
+ StoreIniSection(entry, code.c_str(), ini);
+
+ const bool did_exist = FileSystem::FileExists(save_path);
+ auto fp = FileSystem::OpenManagedCFile(save_path, "wb");
+ if (fp)
+ {
+ // write file comment so simpleini doesn't get confused
+ if (!did_exist)
+ std::fputs("# DuckStation Game Settings\n\n", fp.get());
+
+ SI_Error err = ini.SaveFile(fp.get());
+ if (err != S_OK)
+ Log_ErrorPrintf("Failed to save game settings ini: %d", static_cast(err));
+ }
+ else
+ {
+ Log_ErrorPrintf("Failed to open settings ini for saving: '%s'", save_path);
+ }
+ }
+
+ auto it = m_entries.find(code);
+ if (it != m_entries.end())
+ it->second = entry;
+ else
+ m_entries.emplace(code, entry);
+}
+
+void Entry::ApplySettings(bool display_osd_messages) const
+{
+ constexpr float osd_duration = 10.0f;
+
+ if (display_active_start_offset.has_value())
+ g_settings.display_active_start_offset = display_active_start_offset.value();
+ if (display_active_end_offset.has_value())
+ g_settings.display_active_end_offset = display_active_end_offset.value();
+
+ if (display_crop_mode.has_value())
+ g_settings.display_crop_mode = display_crop_mode.value();
+ if (display_aspect_ratio.has_value())
+ g_settings.display_aspect_ratio = display_aspect_ratio.value();
+ if (controller_1_type.has_value())
+ g_settings.controller_types[0] = controller_1_type.value();
+ if (controller_2_type.has_value())
+ g_settings.controller_types[1] = controller_2_type.value();
+ if (gpu_widescreen_hack.has_value())
+ g_settings.gpu_widescreen_hack = gpu_widescreen_hack.value();
+
+ if (HasTrait(Trait::ForceInterpreter))
+ {
+ if (display_osd_messages && g_settings.cpu_execution_mode != CPUExecutionMode::Interpreter)
+ g_host_interface->AddOSDMessage("CPU execution mode forced to interpreter by game settings.", osd_duration);
+
+ g_settings.cpu_execution_mode = CPUExecutionMode::Interpreter;
+ }
+
+ if (HasTrait(Trait::ForceSoftwareRenderer))
+ {
+ if (display_osd_messages && g_settings.gpu_renderer != GPURenderer::Software)
+ g_host_interface->AddOSDMessage("GPU renderer forced to software by game settings.", osd_duration);
+
+ g_settings.gpu_renderer = GPURenderer::Software;
+ }
+
+ if (HasTrait(Trait::EnableInterlacing))
+ {
+ if (display_osd_messages && g_settings.gpu_disable_interlacing)
+ g_host_interface->AddOSDMessage("Interlacing enabled by game settings.", osd_duration);
+
+ g_settings.gpu_disable_interlacing = false;
+ }
+
+ if (HasTrait(Trait::DisableTrueColor))
+ {
+ if (display_osd_messages && g_settings.gpu_true_color)
+ g_host_interface->AddOSDMessage("True color disabled by game settings.", osd_duration);
+
+ g_settings.gpu_true_color = false;
+ }
+
+ if (HasTrait(Trait::DisableUpscaling))
+ {
+ if (display_osd_messages && g_settings.gpu_resolution_scale > 1)
+ g_host_interface->AddOSDMessage("Upscaling disabled by game settings.", osd_duration);
+
+ g_settings.gpu_resolution_scale = 1;
+ }
+
+ if (HasTrait(Trait::DisableScaledDithering))
+ {
+ if (display_osd_messages && g_settings.gpu_scaled_dithering)
+ g_host_interface->AddOSDMessage("Scaled dithering disabled by game settings.", osd_duration);
+
+ g_settings.gpu_scaled_dithering = false;
+ }
+
+ if (HasTrait(Trait::DisableWidescreen))
+ {
+ if (display_osd_messages &&
+ (g_settings.display_aspect_ratio == DisplayAspectRatio::R16_9 || g_settings.gpu_widescreen_hack))
+ {
+ g_host_interface->AddOSDMessage("Widescreen disabled by game settings.", osd_duration);
+ }
+
+ g_settings.display_aspect_ratio = DisplayAspectRatio::R4_3;
+ g_settings.gpu_widescreen_hack = false;
+ }
+
+ if (HasTrait(Trait::DisablePGXP))
+ {
+ if (display_osd_messages && g_settings.gpu_pgxp_enable)
+ g_host_interface->AddOSDMessage("PGXP geometry correction disabled by game settings.", osd_duration);
+
+ g_settings.gpu_pgxp_enable = false;
+ }
+
+ if (HasTrait(Trait::DisablePGXPCulling))
+ {
+ if (display_osd_messages && g_settings.gpu_pgxp_culling)
+ g_host_interface->AddOSDMessage("PGXP culling disabled by game settings.", osd_duration);
+
+ g_settings.gpu_pgxp_culling = false;
+ }
+
+ if (HasTrait(Trait::EnablePGXPVertexCache))
+ {
+ if (display_osd_messages && g_settings.gpu_pgxp_enable && !g_settings.gpu_pgxp_vertex_cache)
+ g_host_interface->AddOSDMessage("PGXP vertex cache enabled by game settings.", osd_duration);
+
+ g_settings.gpu_pgxp_vertex_cache = true;
+ }
+
+ if (HasTrait(Trait::EnablePGXPCPUMode))
+ {
+ if (display_osd_messages && g_settings.gpu_pgxp_enable && !g_settings.gpu_pgxp_cpu)
+ g_host_interface->AddOSDMessage("PGXP CPU mode enabled by game settings.", osd_duration);
+
+ g_settings.gpu_pgxp_cpu = true;
+ }
+
+ if (HasTrait(Trait::ForceDigitalController))
+ {
+ for (u32 i = 0; i < NUM_CONTROLLER_AND_CARD_PORTS; i++)
+ {
+ if (g_settings.controller_types[i] != ControllerType::None &&
+ g_settings.controller_types[i] != ControllerType::DigitalController)
+ {
+ if (display_osd_messages)
+ {
+ g_host_interface->AddFormattedOSDMessage(osd_duration, "Controller %u changed to digital by game settings.",
+ i + 1u);
+ }
+
+ g_settings.controller_types[i] = ControllerType::DigitalController;
+ }
+ }
+ }
+
+ if (HasTrait(Trait::EnableRecompilerMemoryExceptions))
+ {
+ if (display_osd_messages && g_settings.cpu_execution_mode == CPUExecutionMode::Recompiler &&
+ !g_settings.cpu_recompiler_memory_exceptions)
+ {
+ g_host_interface->AddOSDMessage("Recompiler memory exceptions enabled by game settings.", osd_duration);
+ }
+
+ g_settings.cpu_recompiler_memory_exceptions = true;
+ }
+
+ // TODO: Overscan settings.
+}
+
+} // namespace GameSettings
\ No newline at end of file
diff --git a/src/core/game_settings.h b/src/core/game_settings.h
new file mode 100644
index 000000000..09907dd79
--- /dev/null
+++ b/src/core/game_settings.h
@@ -0,0 +1,74 @@
+#pragma once
+#include "types.h"
+#include
+#include
+#include
+#include
+
+class ByteStream;
+
+namespace GameSettings {
+enum class Trait : u32
+{
+ ForceInterpreter,
+ ForceSoftwareRenderer,
+ EnableInterlacing,
+ DisableTrueColor,
+ DisableUpscaling,
+ DisableScaledDithering,
+ DisableWidescreen,
+ DisablePGXP,
+ DisablePGXPCulling,
+ EnablePGXPVertexCache,
+ EnablePGXPCPUMode,
+ ForceDigitalController,
+ EnableRecompilerMemoryExceptions,
+
+ Count
+};
+
+const char* GetTraitName(Trait trait);
+const char* GetTraitDisplayName(Trait trait);
+
+struct Entry
+{
+ std::bitset(Trait::Count)> traits{};
+ std::optional display_active_start_offset;
+ std::optional display_active_end_offset;
+
+ // user settings
+ std::optional display_crop_mode;
+ std::optional display_aspect_ratio;
+ std::optional controller_1_type;
+ std::optional controller_2_type;
+ std::optional gpu_widescreen_hack;
+
+ ALWAYS_INLINE bool HasTrait(Trait trait) const { return traits[static_cast(trait)]; }
+ ALWAYS_INLINE void AddTrait(Trait trait) { traits[static_cast(trait)] = true; }
+ ALWAYS_INLINE void RemoveTrait(Trait trait) { traits[static_cast(trait)] = false; }
+ ALWAYS_INLINE void SetTrait(Trait trait, bool enabled) { traits[static_cast(trait)] = enabled; }
+
+ bool HasAnySettings() const;
+
+ bool LoadFromStream(ByteStream* stream);
+ bool SaveToStream(ByteStream* stream) const;
+
+ void ApplySettings(bool display_osd_messages) const;
+};
+
+class Database
+{
+public:
+ Database();
+ ~Database();
+
+ const Entry* GetEntry(const std::string& code) const;
+ void SetEntry(const std::string& code, const std::string& name, const Entry& entry, const char* save_path);
+
+ bool Load(const char* path);
+
+private:
+ std::unordered_map m_entries;
+};
+
+}; // namespace GameSettings
\ No newline at end of file
diff --git a/src/core/gpu.cpp b/src/core/gpu.cpp
index 5180eaaa8..c56657c45 100644
--- a/src/core/gpu.cpp
+++ b/src/core/gpu.cpp
@@ -513,15 +513,15 @@ void GPU::UpdateCRTCDisplayParameters()
switch (crop_mode)
{
case DisplayCropMode::None:
- cs.horizontal_active_start = 487;
- cs.horizontal_active_end = 3282;
+ cs.horizontal_active_start = static_cast(std::max(0, 487 + g_settings.display_active_start_offset));
+ cs.horizontal_active_end = static_cast(std::max(0, 3282 + g_settings.display_active_end_offset));
cs.vertical_active_start = 20;
cs.vertical_active_end = 308;
break;
case DisplayCropMode::Overscan:
- cs.horizontal_active_start = 628;
- cs.horizontal_active_end = 3188;
+ cs.horizontal_active_start = static_cast(std::max(0, 628 + g_settings.display_active_start_offset));
+ cs.horizontal_active_end = static_cast(std::max(0, 3188 + g_settings.display_active_end_offset));
cs.vertical_active_start = 30;
cs.vertical_active_end = 298;
break;
@@ -540,15 +540,15 @@ void GPU::UpdateCRTCDisplayParameters()
switch (crop_mode)
{
case DisplayCropMode::None:
- cs.horizontal_active_start = 488;
- cs.horizontal_active_end = 3288;
+ cs.horizontal_active_start = static_cast(std::max(0, 488 + g_settings.display_active_start_offset));
+ cs.horizontal_active_end = static_cast(std::max(0, 3288 + g_settings.display_active_end_offset));
cs.vertical_active_start = 16;
cs.vertical_active_end = 256;
break;
case DisplayCropMode::Overscan:
- cs.horizontal_active_start = 608;
- cs.horizontal_active_end = 3168;
+ cs.horizontal_active_start = static_cast(std::max(0, 608 + g_settings.display_active_start_offset));
+ cs.horizontal_active_end = static_cast(std::max(0, 3168 + g_settings.display_active_end_offset));
cs.vertical_active_start = 24;
cs.vertical_active_end = 248;
break;
@@ -766,7 +766,7 @@ void GPU::CRTCTickEvent(TickCount ticks)
else
{
m_crtc_state.interlaced_field = 0;
- m_GPUSTAT.interlaced_field = 0u; // new GPU = 1, old GPU = 0
+ m_GPUSTAT.interlaced_field = 0u; // new GPU = 1, old GPU = 0
}
}
}
diff --git a/src/core/host_interface.cpp b/src/core/host_interface.cpp
index 26a8634bc..e7f345ca0 100644
--- a/src/core/host_interface.cpp
+++ b/src/core/host_interface.cpp
@@ -378,6 +378,8 @@ void HostInterface::SetDefaultSettings(SettingsInterface& si)
si.SetBoolValue("GPU", "PGXPCPU", false);
si.SetStringValue("Display", "CropMode", Settings::GetDisplayCropModeName(Settings::DEFAULT_DISPLAY_CROP_MODE));
+ si.SetIntValue("Display", "OverscanActiveStartOffset", 0);
+ si.SetIntValue("Display", "OverscanActiveEndOffset", 0);
si.SetStringValue("Display", "AspectRatio",
Settings::GetDisplayAspectRatioName(Settings::DEFAULT_DISPLAY_ASPECT_RATIO));
si.SetBoolValue("Display", "LinearFiltering", true);
@@ -467,7 +469,7 @@ void HostInterface::SaveSettings(SettingsInterface& si)
void HostInterface::CheckForSettingsChanges(const Settings& old_settings)
{
- if (!System::IsShutdown())
+ if (System::IsValid())
{
if (g_settings.gpu_renderer != old_settings.gpu_renderer ||
g_settings.gpu_use_debug_device != old_settings.gpu_use_debug_device)
@@ -522,7 +524,9 @@ void HostInterface::CheckForSettingsChanges(const Settings& old_settings)
g_settings.gpu_force_ntsc_timings != old_settings.gpu_force_ntsc_timings ||
g_settings.display_crop_mode != old_settings.display_crop_mode ||
g_settings.display_aspect_ratio != old_settings.display_aspect_ratio ||
- g_settings.gpu_pgxp_enable != old_settings.gpu_pgxp_enable)
+ g_settings.gpu_pgxp_enable != old_settings.gpu_pgxp_enable ||
+ g_settings.display_active_start_offset != old_settings.display_active_start_offset ||
+ g_settings.display_active_end_offset != old_settings.display_active_end_offset)
{
g_gpu->UpdateSettings();
}
diff --git a/src/core/settings.cpp b/src/core/settings.cpp
index e99dc3c59..d3ba9bbee 100644
--- a/src/core/settings.cpp
+++ b/src/core/settings.cpp
@@ -116,6 +116,8 @@ void Settings::Load(SettingsInterface& si)
ParseDisplayAspectRatio(
si.GetStringValue("Display", "AspectRatio", GetDisplayAspectRatioName(DEFAULT_DISPLAY_ASPECT_RATIO)).c_str())
.value_or(DEFAULT_DISPLAY_ASPECT_RATIO);
+ display_active_start_offset = static_cast(si.GetIntValue("Display", "ActiveStartOffset", 0));
+ display_active_end_offset = static_cast(si.GetIntValue("Display", "ActiveEndOffset", 0));
display_linear_filtering = si.GetBoolValue("Display", "LinearFiltering", true);
display_integer_scaling = si.GetBoolValue("Display", "IntegerScaling", false);
display_show_osd_messages = si.GetBoolValue("Display", "ShowOSDMessages", true);
@@ -219,6 +221,8 @@ void Settings::Save(SettingsInterface& si) const
si.SetBoolValue("GPU", "PGXPCPU", gpu_pgxp_cpu);
si.SetStringValue("Display", "CropMode", GetDisplayCropModeName(display_crop_mode));
+ si.SetIntValue("Display", "ActiveStartOffset", display_active_start_offset);
+ si.SetIntValue("Display", "ActiveEndOffset", display_active_end_offset);
si.SetStringValue("Display", "AspectRatio", GetDisplayAspectRatioName(display_aspect_ratio));
si.SetBoolValue("Display", "LinearFiltering", display_linear_filtering);
si.SetBoolValue("Display", "IntegerScaling", display_integer_scaling);
diff --git a/src/core/settings.h b/src/core/settings.h
index 1aa2f93fa..138e3aa1d 100644
--- a/src/core/settings.h
+++ b/src/core/settings.h
@@ -95,6 +95,8 @@ struct Settings
bool gpu_pgxp_vertex_cache = false;
bool gpu_pgxp_cpu = false;
DisplayCropMode display_crop_mode = DisplayCropMode::None;
+ s16 display_active_start_offset = 0;
+ s16 display_active_end_offset = 0;
DisplayAspectRatio display_aspect_ratio = DisplayAspectRatio::R4_3;
bool display_linear_filtering = true;
bool display_integer_scaling = false;
diff --git a/src/core/system.cpp b/src/core/system.cpp
index 4f3ff2009..ac632b7b1 100644
--- a/src/core/system.cpp
+++ b/src/core/system.cpp
@@ -131,7 +131,7 @@ bool IsShutdown()
bool IsValid()
{
- return s_state != State::Shutdown;
+ return s_state != State::Shutdown && s_state != State::Starting;
}
ConsoleRegion GetRegion()
@@ -384,6 +384,9 @@ bool Boot(const SystemBootParameters& params)
return false;
}
+ // Notify change of disc.
+ UpdateRunningGame(media ? media->GetFileName().c_str() : params.filename.c_str(), media.get());
+
// Component setup.
if (!Initialize(params.force_software_renderer))
{
@@ -391,8 +394,6 @@ bool Boot(const SystemBootParameters& params)
return false;
}
- // Notify change of disc.
- UpdateRunningGame(media ? media->GetFileName().c_str() : params.filename.c_str(), media.get());
UpdateControllers();
UpdateMemoryCards();
Reset();
@@ -1269,6 +1270,9 @@ void RemoveMedia()
void UpdateRunningGame(const char* path, CDImage* image)
{
+ if (s_running_game_path == path)
+ return;
+
s_running_game_path.clear();
s_running_game_code.clear();
s_running_game_title.clear();
diff --git a/src/duckstation-qt/gamepropertiesdialog.cpp b/src/duckstation-qt/gamepropertiesdialog.cpp
index 05bcfd92e..61a6b8fb9 100644
--- a/src/duckstation-qt/gamepropertiesdialog.cpp
+++ b/src/duckstation-qt/gamepropertiesdialog.cpp
@@ -74,6 +74,11 @@ void GamePropertiesDialog::populate(const GameListEntry* ge)
}
populateTracksInfo(ge->path);
+
+ m_game_code = ge->code;
+ m_game_title = ge->title;
+ m_game_settings = ge->settings;
+ populateGameSettings();
}
void GamePropertiesDialog::populateCompatibilityInfo(const std::string& game_code)
@@ -107,6 +112,43 @@ void GamePropertiesDialog::setupAdditionalUi()
tr(GameList::GetGameListCompatibilityRatingString(static_cast(i))));
}
+ m_ui.userAspectRatio->addItem(tr("(unchanged)"));
+ for (u32 i = 0; i < static_cast(DisplayAspectRatio::Count); i++)
+ {
+ m_ui.userAspectRatio->addItem(
+ QString::fromUtf8(Settings::GetDisplayAspectRatioName(static_cast(i))));
+ }
+
+ m_ui.userCropMode->addItem(tr("(unchanged)"));
+ for (u32 i = 0; i < static_cast(DisplayCropMode::Count); i++)
+ {
+ m_ui.userCropMode->addItem(
+ QString::fromUtf8(Settings::GetDisplayCropModeDisplayName(static_cast(i))));
+ }
+
+ m_ui.userControllerType1->addItem(tr("(unchanged)"));
+ for (u32 i = 0; i < static_cast(ControllerType::Count); i++)
+ {
+ m_ui.userControllerType1->addItem(
+ QString::fromUtf8(Settings::GetControllerTypeDisplayName(static_cast(i))));
+ }
+
+ m_ui.userControllerType2->addItem(tr("(unchanged)"));
+ for (u32 i = 0; i < static_cast(ControllerType::Count); i++)
+ {
+ m_ui.userControllerType2->addItem(
+ QString::fromUtf8(Settings::GetControllerTypeDisplayName(static_cast(i))));
+ }
+
+ QGridLayout* traits_layout = new QGridLayout(m_ui.compatibilityTraits);
+ for (u32 i = 0; i < static_cast(GameSettings::Trait::Count); i++)
+ {
+ m_trait_checkboxes[i] =
+ new QCheckBox(QString::fromUtf8(GameSettings::GetTraitDisplayName(static_cast(i))),
+ m_ui.compatibilityTraits);
+ traits_layout->addWidget(m_trait_checkboxes[i], i / 2, i % 2);
+ }
+
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
}
@@ -133,7 +175,7 @@ void GamePropertiesDialog::populateTracksInfo(const std::string& image_path)
{"Audio", "Mode 1", "Mode 1/Raw", "Mode 2", "Mode 2/Form 1", "Mode 2/Form 2", "Mode 2/Mix", "Mode 2/Raw"}};
m_ui.tracks->clearContents();
- m_image_path = image_path;
+ m_path = image_path;
std::unique_ptr image = CDImage::Open(image_path.c_str());
if (!image)
@@ -155,6 +197,66 @@ void GamePropertiesDialog::populateTracksInfo(const std::string& image_path)
}
}
+void GamePropertiesDialog::populateGameSettings()
+{
+ const GameSettings::Entry& gs = m_game_settings;
+
+ for (u32 i = 0; i < static_cast(GameSettings::Trait::Count); i++)
+ {
+ QSignalBlocker sb(m_trait_checkboxes[i]);
+ m_trait_checkboxes[i]->setChecked(gs.HasTrait(static_cast(i)));
+ }
+
+ if (gs.display_active_start_offset.has_value())
+ {
+ QSignalBlocker sb(m_ui.displayActiveStartOffset);
+ m_ui.displayActiveStartOffset->setValue(static_cast(gs.display_active_start_offset.value()));
+ }
+ if (gs.display_active_end_offset.has_value())
+ {
+ QSignalBlocker sb(m_ui.displayActiveEndOffset);
+ m_ui.displayActiveEndOffset->setValue(static_cast(gs.display_active_end_offset.value()));
+ }
+
+ if (gs.display_crop_mode.has_value())
+ {
+ QSignalBlocker sb(m_ui.userCropMode);
+ m_ui.userCropMode->setCurrentIndex(static_cast(gs.display_crop_mode.value()) + 1);
+ }
+ if (gs.display_aspect_ratio.has_value())
+ {
+ QSignalBlocker sb(m_ui.userAspectRatio);
+ m_ui.userAspectRatio->setCurrentIndex(static_cast(gs.display_aspect_ratio.value()) + 1);
+ }
+
+ if (gs.controller_1_type.has_value())
+ {
+ QSignalBlocker sb(m_ui.userControllerType1);
+ m_ui.userControllerType1->setCurrentIndex(static_cast(gs.controller_1_type.value()) + 1);
+ }
+ if (gs.controller_2_type.has_value())
+ {
+ QSignalBlocker sb(m_ui.userControllerType2);
+ m_ui.userControllerType2->setCurrentIndex(static_cast(gs.controller_2_type.value()) + 1);
+ }
+ if (gs.gpu_widescreen_hack.has_value())
+ {
+ QSignalBlocker sb(m_ui.userControllerType2);
+ m_ui.userWidescreenHack->setCheckState(Qt::Checked);
+ }
+ else
+ {
+ QSignalBlocker sb(m_ui.userControllerType2);
+ m_ui.userWidescreenHack->setCheckState(Qt::PartiallyChecked);
+ }
+}
+
+void GamePropertiesDialog::saveGameSettings()
+{
+ m_host_interface->getGameList()->UpdateGameSettings(m_path, m_game_code, m_game_title, m_game_settings, true, true);
+ m_host_interface->applySettings(true);
+}
+
void GamePropertiesDialog::closeEvent(QCloseEvent* ev)
{
deleteLater();
@@ -186,6 +288,69 @@ void GamePropertiesDialog::connectUi()
connect(m_ui.exportCompatibilityInfo, &QPushButton::clicked, this,
&GamePropertiesDialog::onExportCompatibilityInfoClicked);
connect(m_ui.close, &QPushButton::clicked, this, &QDialog::close);
+
+ connect(m_ui.userAspectRatio, QOverload::of(&QComboBox::currentIndexChanged), [this](int index) {
+ if (index <= 0)
+ m_game_settings.display_aspect_ratio.reset();
+ else
+ m_game_settings.display_aspect_ratio = static_cast(index - 1);
+ saveGameSettings();
+ });
+
+ connect(m_ui.userCropMode, QOverload::of(&QComboBox::currentIndexChanged), [this](int index) {
+ if (index <= 0)
+ m_game_settings.display_crop_mode.reset();
+ else
+ m_game_settings.display_crop_mode = static_cast(index - 1);
+ saveGameSettings();
+ });
+
+ connect(m_ui.userControllerType1, QOverload::of(&QComboBox::currentIndexChanged), [this](int index) {
+ if (index <= 0)
+ m_game_settings.controller_1_type.reset();
+ else
+ m_game_settings.controller_1_type = static_cast(index - 1);
+ saveGameSettings();
+ });
+
+ connect(m_ui.userControllerType2, QOverload::of(&QComboBox::currentIndexChanged), [this](int index) {
+ if (index <= 0)
+ m_game_settings.controller_2_type.reset();
+ else
+ m_game_settings.controller_2_type = static_cast(index - 1);
+ saveGameSettings();
+ });
+
+ connect(m_ui.userWidescreenHack, &QCheckBox::stateChanged, [this](int state) {
+ if (state == Qt::PartiallyChecked)
+ m_game_settings.gpu_widescreen_hack.reset();
+ else
+ m_game_settings.gpu_widescreen_hack = (state == Qt::Checked);
+ saveGameSettings();
+ });
+
+ for (u32 i = 0; i < static_cast(GameSettings::Trait::Count); i++)
+ {
+ connect(m_trait_checkboxes[i], &QCheckBox::toggled, [this, i](bool checked) {
+ m_game_settings.SetTrait(static_cast(i), checked);
+ saveGameSettings();
+ });
+ }
+
+ connect(m_ui.displayActiveStartOffset, QOverload::of(&QSpinBox::valueChanged), [this](int value) {
+ if (value == 0)
+ m_game_settings.display_active_start_offset.reset();
+ else
+ m_game_settings.display_active_start_offset = static_cast(value);
+ saveGameSettings();
+ });
+ connect(m_ui.displayActiveEndOffset, QOverload::of(&QSpinBox::valueChanged), [this](int value) {
+ if (value == 0)
+ m_game_settings.display_active_end_offset.reset();
+ else
+ m_game_settings.display_active_end_offset = static_cast(value);
+ saveGameSettings();
+ });
}
void GamePropertiesDialog::fillEntryFromUi(GameListCompatibilityEntry* entry)
@@ -263,10 +428,10 @@ void GamePropertiesDialog::onExportCompatibilityInfoClicked()
void GamePropertiesDialog::computeTrackHashes()
{
- if (m_image_path.empty())
+ if (m_path.empty())
return;
- std::unique_ptr image = CDImage::Open(m_image_path.c_str());
+ std::unique_ptr image = CDImage::Open(m_path.c_str());
if (!image)
return;
diff --git a/src/duckstation-qt/gamepropertiesdialog.h b/src/duckstation-qt/gamepropertiesdialog.h
index 724fd785f..fb2929bec 100644
--- a/src/duckstation-qt/gamepropertiesdialog.h
+++ b/src/duckstation-qt/gamepropertiesdialog.h
@@ -1,6 +1,8 @@
#pragma once
+#include "core/game_settings.h"
#include "ui_gamepropertiesdialog.h"
#include
+#include
struct GameListEntry;
struct GameListCompatibilityEntry;
@@ -40,15 +42,22 @@ private:
void connectUi();
void populateCompatibilityInfo(const std::string& game_code);
void populateTracksInfo(const std::string& image_path);
+ void populateGameSettings();
+ void saveGameSettings();
void fillEntryFromUi(GameListCompatibilityEntry* entry);
void computeTrackHashes();
void onResize();
Ui::GamePropertiesDialog m_ui;
+ std::array(GameSettings::Trait::Count)> m_trait_checkboxes{};
QtHostInterface* m_host_interface;
- std::string m_image_path;
+ std::string m_path;
+ std::string m_game_code;
+ std::string m_game_title;
+
+ GameSettings::Entry m_game_settings;
bool m_compatibility_info_changed = false;
bool m_tracks_hashed = false;
diff --git a/src/duckstation-qt/gamepropertiesdialog.ui b/src/duckstation-qt/gamepropertiesdialog.ui
index 167e7af70..e6a800ede 100644
--- a/src/duckstation-qt/gamepropertiesdialog.ui
+++ b/src/duckstation-qt/gamepropertiesdialog.ui
@@ -6,171 +6,346 @@
0
0
- 722
- 466
+ 793
+ 647
Dialog
-
+
:/icons/duck.png:/icons/duck.png
-
- -
-
-
- Image Path:
+
+
-
+
+
+ 0
+
+
+ Properties
+
+
+
-
+
+
+ Image Path:
+
+
+
+ -
+
+
+ true
+
+
+
+ -
+
+
+ Game Code:
+
+
+
+ -
+
+
+ true
+
+
+
+ -
+
+
+ Title:
+
+
+
+ -
+
+
+ true
+
+
+
+ -
+
+
+ Region:
+
+
+
+ -
+
+
+ false
+
+
+
+ -
+
+
+ Compatibility:
+
+
+
+ -
+
+
+ -
+
+
+ Upscaling Issues:
+
+
+
+ -
+
+
+ -
+
+
+ Comments:
+
+
+
+ -
+
+
+ -
+
+
+ Version Tested:
+
+
+
+ -
+
+
-
+
+
+ -
+
+
+ Set to Current
+
+
+
+
+
+ -
+
+
+ Tracks:
+
+
+
+ -
+
+
+ QAbstractItemView::NoEditTriggers
+
+
+ false
+
+
+ false
+
+
+
+ #
+
+
+
+
+ Mode
+
+
+
+
+ Start
+
+
+
+
+ Length
+
+
+
+
+ Hash
+
+
+
+
+
+
+
+
+ User Settings
+
+
+ -
+
+
+ GPU Settings
+
+
+
-
+
+
+ Crop Mode:
+
+
+
+ -
+
+
+ -
+
+
+ Aspect Ratio:
+
+
+
+ -
+
+
+ -
+
+
+ Widescreen Hack
+
+
+ true
+
+
+
+
+
+
+ -
+
+
+ Controller Settings
+
+
+
-
+
+
+ Controller 1 Type:
+
+
+
+ -
+
+
+ -
+
+
+ Controller 2 Type:
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+ Compatibility Settings
+
+
+ -
+
+
+ Traits
+
+
+
+ -
+
+
+ Overrides
+
+
+
-
+
+
+ Display Active Offset:
+
+
+
+ -
+
+
-
+
+
+ -5000
+
+
+ 5000
+
+
+ 0
+
+
+
+ -
+
+
+ -5000
+
+
+ 5000
+
+
+ 0
+
+
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
- -
-
-
- true
-
-
-
- -
-
-
- Game Code:
-
-
-
- -
-
-
- true
-
-
-
- -
-
-
- Title:
-
-
-
- -
-
-
- true
-
-
-
- -
-
-
- Region:
-
-
-
- -
-
-
- false
-
-
-
- -
-
-
- Compatibility:
-
-
-
- -
-
-
- -
-
-
- Upscaling Issues:
-
-
-
- -
-
-
- -
-
-
- Comments:
-
-
-
- -
-
-
- -
-
-
- Version Tested:
-
-
-
- -
-
-
-
-
-
- -
-
-
- Set to Current
-
-
-
-
-
- -
-
-
- Tracks:
-
-
-
- -
-
-
- QAbstractItemView::NoEditTriggers
-
-
- false
-
-
- false
-
-
-
- #
-
-
-
-
- Mode
-
-
-
-
- Start
-
-
-
-
- Length
-
-
-
-
- Hash
-
-
-
-
- -
+
-
-
diff --git a/src/duckstation-qt/qthostinterface.cpp b/src/duckstation-qt/qthostinterface.cpp
index 3d5b82927..fbfed9411 100644
--- a/src/duckstation-qt/qthostinterface.cpp
+++ b/src/duckstation-qt/qthostinterface.cpp
@@ -280,16 +280,17 @@ void QtHostInterface::setDefaultSettings()
m_settings_interface->Save();
CommonHostInterface::LoadSettings(*m_settings_interface.get());
+ CommonHostInterface::ApplyGameSettings(false);
}
CheckForSettingsChanges(old_settings);
}
-void QtHostInterface::applySettings()
+void QtHostInterface::applySettings(bool display_osd_messages /* = false */)
{
if (!isOnWorkerThread())
{
- QMetaObject::invokeMethod(this, "applySettings", Qt::QueuedConnection);
+ QMetaObject::invokeMethod(this, "applySettings", Qt::QueuedConnection, Q_ARG(bool, display_osd_messages));
return;
}
@@ -297,6 +298,7 @@ void QtHostInterface::applySettings()
{
std::lock_guard guard(m_settings_mutex);
CommonHostInterface::LoadSettings(*m_settings_interface.get());
+ CommonHostInterface::ApplyGameSettings(display_osd_messages);
}
CheckForSettingsChanges(old_settings);
@@ -649,6 +651,7 @@ void QtHostInterface::OnSystemPerformanceCountersUpdated()
void QtHostInterface::OnRunningGameChanged()
{
CommonHostInterface::OnRunningGameChanged();
+ applySettings(true);
if (!System::IsShutdown())
{
diff --git a/src/duckstation-qt/qthostinterface.h b/src/duckstation-qt/qthostinterface.h
index 5f843364c..294aa2225 100644
--- a/src/duckstation-qt/qthostinterface.h
+++ b/src/duckstation-qt/qthostinterface.h
@@ -129,7 +129,7 @@ Q_SIGNALS:
public Q_SLOTS:
void setDefaultSettings();
- void applySettings();
+ void applySettings(bool display_osd_messages = false);
void updateInputMap();
void applyInputProfile(const QString& profile_path);
void onDisplayWindowKeyEvent(int key, bool pressed);
diff --git a/src/duckstation-sdl/sdl_host_interface.cpp b/src/duckstation-sdl/sdl_host_interface.cpp
index 85c223461..061e0abbb 100644
--- a/src/duckstation-sdl/sdl_host_interface.cpp
+++ b/src/duckstation-sdl/sdl_host_interface.cpp
@@ -321,6 +321,11 @@ void SDLHostInterface::OnRunningGameChanged()
{
CommonHostInterface::OnRunningGameChanged();
+ Settings old_settings(std::move(g_settings));
+ CommonHostInterface::LoadSettings(*m_settings_interface.get());
+ CommonHostInterface::ApplyGameSettings(true);
+ CheckForSettingsChanges(old_settings);
+
if (!System::GetRunningTitle().empty())
SDL_SetWindowTitle(m_window, System::GetRunningTitle().c_str());
else
@@ -347,6 +352,7 @@ void SDLHostInterface::SaveAndUpdateSettings()
Settings old_settings(std::move(g_settings));
CommonHostInterface::LoadSettings(*m_settings_interface.get());
+ CommonHostInterface::ApplyGameSettings(false);
CheckForSettingsChanges(old_settings);
m_settings_interface->Save();
diff --git a/src/frontend-common/common_host_interface.cpp b/src/frontend-common/common_host_interface.cpp
index c17dd9669..17badd8c1 100644
--- a/src/frontend-common/common_host_interface.cpp
+++ b/src/frontend-common/common_host_interface.cpp
@@ -71,6 +71,8 @@ bool CommonHostInterface::Initialize()
m_game_list->SetCacheFilename(GetUserDirectoryRelativePath("cache/gamelist.cache"));
m_game_list->SetDatabaseFilename(GetUserDirectoryRelativePath("cache/redump.dat"));
m_game_list->SetCompatibilityFilename(GetProgramDirectoryRelativePath("database/compatibility.xml"));
+ m_game_list->SetGameSettingsFilename(GetProgramDirectoryRelativePath("database/gamesettings.ini"));
+ m_game_list->SetUserGameSettingsFilename(GetUserDirectoryRelativePath("gamesettings.ini"));
m_save_state_selector_ui = std::make_unique(this);
@@ -2024,6 +2026,17 @@ bool CommonHostInterface::SaveScreenshot(const char* filename /* = nullptr */, b
return true;
}
+void CommonHostInterface::ApplyGameSettings(bool display_osd_messages)
+{
+ // this gets called while booting, so can't use valid
+ if (System::IsShutdown() || System::GetRunningCode().empty())
+ return;
+
+ const GameSettings::Entry* gs = m_game_list->GetGameSettings(System::GetRunningPath(), System::GetRunningCode());
+ if (gs)
+ gs->ApplySettings(display_osd_messages);
+}
+
#ifdef WITH_DISCORD_PRESENCE
void CommonHostInterface::SetDiscordPresenceEnabled(bool enabled)
diff --git a/src/frontend-common/common_host_interface.h b/src/frontend-common/common_host_interface.h
index 16be28a6e..b365f5dd3 100644
--- a/src/frontend-common/common_host_interface.h
+++ b/src/frontend-common/common_host_interface.h
@@ -266,6 +266,8 @@ protected:
void RecreateSystem() override;
+ void ApplyGameSettings(bool display_osd_messages);
+
virtual void DrawImGuiWindows();
void DrawFPSWindow();