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();