Add per-game overrides (mainly for compatibility)

This commit is contained in:
Connor McLaughlin 2020-08-20 21:30:11 +10:00
parent e8da20f174
commit 60d3fffec1
21 changed files with 1172 additions and 185 deletions

View file

@ -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.

View file

@ -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)

View file

@ -60,6 +60,7 @@
<ClCompile Include="cpu_types.cpp" />
<ClCompile Include="digital_controller.cpp" />
<ClCompile Include="game_list.cpp" />
<ClCompile Include="game_settings.cpp" />
<ClCompile Include="gpu_commands.cpp" />
<ClCompile Include="gpu_hw_d3d11.cpp" />
<ClCompile Include="gpu_hw_shadergen.cpp" />
@ -107,6 +108,7 @@
<ClInclude Include="cpu_recompiler_types.h" />
<ClInclude Include="digital_controller.h" />
<ClInclude Include="game_list.h" />
<ClInclude Include="game_settings.h" />
<ClInclude Include="gpu_hw_d3d11.h" />
<ClInclude Include="gpu_hw_shadergen.h" />
<ClInclude Include="gpu_hw_vulkan.h" />
@ -142,6 +144,12 @@
<ClInclude Include="types.h" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\dep\imgui\imgui.vcxproj">
<Project>{bb08260f-6fbc-46af-8924-090ee71360c6}</Project>
</ProjectReference>
<ProjectReference Include="..\..\dep\simpleini\simpleini.vcxproj">
<Project>{3773f4cc-614e-4028-8595-22e08ca649e3}</Project>
</ProjectReference>
<ProjectReference Include="..\..\dep\stb\stb.vcxproj">
<Project>{ed601289-ac1a-46b8-a8ed-17db9eb73423}</Project>
</ProjectReference>
@ -299,7 +307,7 @@
<PreprocessorDefinitions>WITH_RECOMPILER=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<SDLCheck>true</SDLCheck>
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
<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\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<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)</AdditionalIncludeDirectories>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<MinimalRebuild>false</MinimalRebuild>
<LanguageStandard>stdcpp17</LanguageStandard>
@ -325,7 +333,7 @@
<PreprocessorDefinitions>WITH_RECOMPILER=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<SDLCheck>true</SDLCheck>
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
<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\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<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)</AdditionalIncludeDirectories>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<MinimalRebuild>false</MinimalRebuild>
<LanguageStandard>stdcpp17</LanguageStandard>
@ -351,7 +359,7 @@
<PreprocessorDefinitions>WITH_RECOMPILER=1;_ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<SDLCheck>true</SDLCheck>
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
<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\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<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)</AdditionalIncludeDirectories>
<BasicRuntimeChecks>Default</BasicRuntimeChecks>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<MinimalRebuild>false</MinimalRebuild>
@ -380,7 +388,7 @@
<PreprocessorDefinitions>WITH_RECOMPILER=1;_ITERATOR_DEBUG_LEVEL=1;_CRT_SECURE_NO_WARNINGS;WIN32;_DEBUGFAST;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<SDLCheck>true</SDLCheck>
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
<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\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<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)</AdditionalIncludeDirectories>
<BasicRuntimeChecks>Default</BasicRuntimeChecks>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<MinimalRebuild>false</MinimalRebuild>
@ -408,7 +416,7 @@
<Optimization>MaxSpeed</Optimization>
<IntrinsicFunctions>true</IntrinsicFunctions>
<PreprocessorDefinitions>WITH_RECOMPILER=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<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\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<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)</AdditionalIncludeDirectories>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<WholeProgramOptimization>false</WholeProgramOptimization>
<LanguageStandard>stdcpp17</LanguageStandard>
@ -435,7 +443,7 @@
<Optimization>MaxSpeed</Optimization>
<IntrinsicFunctions>true</IntrinsicFunctions>
<PreprocessorDefinitions>WITH_RECOMPILER=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<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\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<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)</AdditionalIncludeDirectories>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<WholeProgramOptimization>true</WholeProgramOptimization>
<LanguageStandard>stdcpp17</LanguageStandard>
@ -463,7 +471,7 @@
<Optimization>MaxSpeed</Optimization>
<IntrinsicFunctions>true</IntrinsicFunctions>
<PreprocessorDefinitions>WITH_RECOMPILER=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<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\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<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)</AdditionalIncludeDirectories>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<WholeProgramOptimization>false</WholeProgramOptimization>
<LanguageStandard>stdcpp17</LanguageStandard>
@ -490,7 +498,7 @@
<Optimization>MaxSpeed</Optimization>
<IntrinsicFunctions>true</IntrinsicFunctions>
<PreprocessorDefinitions>WITH_RECOMPILER=1;_CRT_SECURE_NO_WARNINGS;WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<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\zlib\include;$(SolutionDir)dep\vulkan-loader\include;$(SolutionDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<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)</AdditionalIncludeDirectories>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<WholeProgramOptimization>true</WholeProgramOptimization>
<LanguageStandard>stdcpp17</LanguageStandard>

View file

@ -47,6 +47,7 @@
<ClCompile Include="resources.cpp" />
<ClCompile Include="host_interface_progress_callback.cpp" />
<ClCompile Include="pgxp.cpp" />
<ClCompile Include="game_settings.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="types.h" />
@ -97,5 +98,6 @@
<ClInclude Include="gte_types.h" />
<ClInclude Include="pgxp.h" />
<ClInclude Include="cpu_core_private.h" />
<ClInclude Include="game_settings.h" />
</ItemGroup>
</Project>

View file

@ -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<GameListEntryType>(type);
ge.compatibility_rating = static_cast<GameListCompatibilityRating>(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<u8>(entry->region));
result &= WriteU8(stream, static_cast<u8>(entry->type));
result &= WriteU8(stream, static_cast<u8>(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());
}
}

View file

@ -1,4 +1,5 @@
#pragma once
#include "game_settings.h"
#include "types.h"
#include <memory>
#include <optional>
@ -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<std::string, GameListDatabaseEntry>;
@ -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<ByteStream> m_cache_write_stream;
std::vector<DirectoryEntry> 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;
};

429
src/core/game_settings.cpp Normal file
View file

@ -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 <array>
#include <utility>
Log_SetChannel(GameSettings);
#ifdef WIN32
#include "common/windows_headers.h"
#endif
#include "SimpleIni.h"
namespace GameSettings {
std::array<std::pair<const char*, const char*>, static_cast<u32>(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<u32>(trait)].first;
}
const char* GetTraitDisplayName(Trait trait)
{
DebugAssert(trait < Trait::Count);
return s_trait_names[static_cast<u32>(trait)].second;
}
bool Entry::HasAnySettings() const
{
return traits.any() || display_active_start_offset >= 0 || display_active_end_offset > 0;
}
template<typename T>
bool ReadOptionalFromStream(ByteStream* stream, std::optional<T>* 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<typename T>
bool WriteOptionalToStream(ByteStream* stream, const std::optional<T>& 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<u32>(Trait::Count) + 7) / 8;
std::array<u8, num_bytes> 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<int>(Trait::Count); i++)
{
if ((bits[i / 8] & (1u << (i % 8))) != 0)
AddTrait(static_cast<Trait>(i));
}
return true;
}
bool Entry::SaveToStream(ByteStream* stream) const
{
constexpr u32 num_bytes = (static_cast<u32>(Trait::Count) + 7) / 8;
std::array<u8, num_bytes> bits;
bits.fill(0);
for (u32 i = 0; i < static_cast<int>(Trait::Count); i++)
{
if (HasTrait(static_cast<Trait>(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<u32>(Trait::Count); trait++)
{
if (ini.GetBoolValue(section, s_trait_names[trait].first, false))
entry->AddTrait(static_cast<Trait>(trait));
}
long lvalue = ini.GetLongValue(section, "DisplayActiveStartOffset", 0);
if (lvalue != 0)
entry->display_active_start_offset = static_cast<s16>(lvalue);
lvalue = ini.GetLongValue(section, "DisplayActiveEndOffset", 0);
if (lvalue != 0)
entry->display_active_end_offset = static_cast<s16>(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<bool>(cvalue);
}
static void StoreIniSection(const Entry& entry, const char* section, CSimpleIniA& ini)
{
for (u32 trait = 0; trait < static_cast<u32>(Trait::Count); trait++)
{
if (entry.HasTrait(static_cast<Trait>(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<int>(err));
return false;
}
std::list<CSimpleIniA::Entry> 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<int>(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<int>(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

74
src/core/game_settings.h Normal file
View file

@ -0,0 +1,74 @@
#pragma once
#include "types.h"
#include <bitset>
#include <optional>
#include <string>
#include <unordered_map>
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<static_cast<int>(Trait::Count)> traits{};
std::optional<s16> display_active_start_offset;
std::optional<s16> display_active_end_offset;
// user settings
std::optional<DisplayCropMode> display_crop_mode;
std::optional<DisplayAspectRatio> display_aspect_ratio;
std::optional<ControllerType> controller_1_type;
std::optional<ControllerType> controller_2_type;
std::optional<bool> gpu_widescreen_hack;
ALWAYS_INLINE bool HasTrait(Trait trait) const { return traits[static_cast<int>(trait)]; }
ALWAYS_INLINE void AddTrait(Trait trait) { traits[static_cast<int>(trait)] = true; }
ALWAYS_INLINE void RemoveTrait(Trait trait) { traits[static_cast<int>(trait)] = false; }
ALWAYS_INLINE void SetTrait(Trait trait, bool enabled) { traits[static_cast<int>(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<std::string, Entry> m_entries;
};
}; // namespace GameSettings

View file

@ -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<u16>(std::max<int>(0, 487 + g_settings.display_active_start_offset));
cs.horizontal_active_end = static_cast<u16>(std::max<int>(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<u16>(std::max<int>(0, 628 + g_settings.display_active_start_offset));
cs.horizontal_active_end = static_cast<u16>(std::max<int>(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<u16>(std::max<int>(0, 488 + g_settings.display_active_start_offset));
cs.horizontal_active_end = static_cast<u16>(std::max<int>(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<u16>(std::max<int>(0, 608 + g_settings.display_active_start_offset));
cs.horizontal_active_end = static_cast<u16>(std::max<int>(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
}
}
}

View file

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

View file

@ -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<s16>(si.GetIntValue("Display", "ActiveStartOffset", 0));
display_active_end_offset = static_cast<s16>(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);

View file

@ -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;

View file

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

View file

@ -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<GameListCompatibilityRating>(i))));
}
m_ui.userAspectRatio->addItem(tr("(unchanged)"));
for (u32 i = 0; i < static_cast<u32>(DisplayAspectRatio::Count); i++)
{
m_ui.userAspectRatio->addItem(
QString::fromUtf8(Settings::GetDisplayAspectRatioName(static_cast<DisplayAspectRatio>(i))));
}
m_ui.userCropMode->addItem(tr("(unchanged)"));
for (u32 i = 0; i < static_cast<u32>(DisplayCropMode::Count); i++)
{
m_ui.userCropMode->addItem(
QString::fromUtf8(Settings::GetDisplayCropModeDisplayName(static_cast<DisplayCropMode>(i))));
}
m_ui.userControllerType1->addItem(tr("(unchanged)"));
for (u32 i = 0; i < static_cast<u32>(ControllerType::Count); i++)
{
m_ui.userControllerType1->addItem(
QString::fromUtf8(Settings::GetControllerTypeDisplayName(static_cast<ControllerType>(i))));
}
m_ui.userControllerType2->addItem(tr("(unchanged)"));
for (u32 i = 0; i < static_cast<u32>(ControllerType::Count); i++)
{
m_ui.userControllerType2->addItem(
QString::fromUtf8(Settings::GetControllerTypeDisplayName(static_cast<ControllerType>(i))));
}
QGridLayout* traits_layout = new QGridLayout(m_ui.compatibilityTraits);
for (u32 i = 0; i < static_cast<u32>(GameSettings::Trait::Count); i++)
{
m_trait_checkboxes[i] =
new QCheckBox(QString::fromUtf8(GameSettings::GetTraitDisplayName(static_cast<GameSettings::Trait>(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<CDImage> 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<u32>(GameSettings::Trait::Count); i++)
{
QSignalBlocker sb(m_trait_checkboxes[i]);
m_trait_checkboxes[i]->setChecked(gs.HasTrait(static_cast<GameSettings::Trait>(i)));
}
if (gs.display_active_start_offset.has_value())
{
QSignalBlocker sb(m_ui.displayActiveStartOffset);
m_ui.displayActiveStartOffset->setValue(static_cast<int>(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<int>(gs.display_active_end_offset.value()));
}
if (gs.display_crop_mode.has_value())
{
QSignalBlocker sb(m_ui.userCropMode);
m_ui.userCropMode->setCurrentIndex(static_cast<int>(gs.display_crop_mode.value()) + 1);
}
if (gs.display_aspect_ratio.has_value())
{
QSignalBlocker sb(m_ui.userAspectRatio);
m_ui.userAspectRatio->setCurrentIndex(static_cast<int>(gs.display_aspect_ratio.value()) + 1);
}
if (gs.controller_1_type.has_value())
{
QSignalBlocker sb(m_ui.userControllerType1);
m_ui.userControllerType1->setCurrentIndex(static_cast<int>(gs.controller_1_type.value()) + 1);
}
if (gs.controller_2_type.has_value())
{
QSignalBlocker sb(m_ui.userControllerType2);
m_ui.userControllerType2->setCurrentIndex(static_cast<int>(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<int>::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<DisplayAspectRatio>(index - 1);
saveGameSettings();
});
connect(m_ui.userCropMode, QOverload<int>::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<DisplayCropMode>(index - 1);
saveGameSettings();
});
connect(m_ui.userControllerType1, QOverload<int>::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<ControllerType>(index - 1);
saveGameSettings();
});
connect(m_ui.userControllerType2, QOverload<int>::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<ControllerType>(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<u32>(GameSettings::Trait::Count); i++)
{
connect(m_trait_checkboxes[i], &QCheckBox::toggled, [this, i](bool checked) {
m_game_settings.SetTrait(static_cast<GameSettings::Trait>(i), checked);
saveGameSettings();
});
}
connect(m_ui.displayActiveStartOffset, QOverload<int>::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<s16>(value);
saveGameSettings();
});
connect(m_ui.displayActiveEndOffset, QOverload<int>::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<s16>(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<CDImage> image = CDImage::Open(m_image_path.c_str());
std::unique_ptr<CDImage> image = CDImage::Open(m_path.c_str());
if (!image)
return;

View file

@ -1,6 +1,8 @@
#pragma once
#include "core/game_settings.h"
#include "ui_gamepropertiesdialog.h"
#include <QtWidgets/QDialog>
#include <array>
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<QCheckBox*, static_cast<u32>(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;

View file

@ -6,171 +6,346 @@
<rect>
<x>0</x>
<y>0</y>
<width>722</width>
<height>466</height>
<width>793</width>
<height>647</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<property name="windowIcon">
<iconset resource="resources/icons.qrc">
<iconset>
<normaloff>:/icons/duck.png</normaloff>:/icons/duck.png</iconset>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Image Path:</string>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tab">
<attribute name="title">
<string>Properties</string>
</attribute>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Image Path:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="imagePath">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Game Code:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="gameCode">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Title:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="title">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Region:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="region">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Compatibility:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QComboBox" name="compatibility"/>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Upscaling Issues:</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QLineEdit" name="upscalingIssues"/>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Comments:</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QLineEdit" name="comments"/>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_9">
<property name="text">
<string>Version Tested:</string>
</property>
</widget>
</item>
<item row="7" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLineEdit" name="versionTested"/>
</item>
<item>
<widget class="QPushButton" name="setToCurrent">
<property name="text">
<string>Set to Current</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="8" column="0" colspan="2">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Tracks:</string>
</property>
</widget>
</item>
<item row="9" column="0" colspan="2">
<widget class="QTableWidget" name="tracks">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="cornerButtonEnabled">
<bool>false</bool>
</property>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string>#</string>
</property>
</column>
<column>
<property name="text">
<string>Mode</string>
</property>
</column>
<column>
<property name="text">
<string>Start</string>
</property>
</column>
<column>
<property name="text">
<string>Length</string>
</property>
</column>
<column>
<property name="text">
<string>Hash</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_3">
<attribute name="title">
<string>User Settings</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QGroupBox" name="groupBox_4">
<property name="title">
<string>GPU Settings</string>
</property>
<layout class="QFormLayout" name="formLayout_5">
<item row="0" column="0">
<widget class="QLabel" name="label_17">
<property name="text">
<string>Crop Mode:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="userCropMode"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_18">
<property name="text">
<string>Aspect Ratio:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="userAspectRatio"/>
</item>
<item row="2" column="0" colspan="2">
<widget class="QCheckBox" name="userWidescreenHack">
<property name="text">
<string>Widescreen Hack</string>
</property>
<property name="tristate">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Controller Settings</string>
</property>
<layout class="QFormLayout" name="formLayout_4">
<item row="0" column="0">
<widget class="QLabel" name="label_15">
<property name="text">
<string>Controller 1 Type:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="userControllerType1"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_16">
<property name="text">
<string>Controller 2 Type:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="userControllerType2"/>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_2">
<attribute name="title">
<string>Compatibility Settings</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QGroupBox" name="compatibilityTraits">
<property name="title">
<string>Traits</string>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="compatibilityOverrides">
<property name="title">
<string>Overrides</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_10">
<property name="text">
<string>Display Active Offset:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QSpinBox" name="displayActiveStartOffset">
<property name="minimum">
<number>-5000</number>
</property>
<property name="maximum">
<number>5000</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="displayActiveEndOffset">
<property name="minimum">
<number>-5000</number>
</property>
<property name="maximum">
<number>5000</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="imagePath">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Game Code:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="gameCode">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Title:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="title">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Region:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="region">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Compatibility:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QComboBox" name="compatibility"/>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Upscaling Issues:</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QLineEdit" name="upscalingIssues"/>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Comments:</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QLineEdit" name="comments"/>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_9">
<property name="text">
<string>Version Tested:</string>
</property>
</widget>
</item>
<item row="7" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLineEdit" name="versionTested"/>
</item>
<item>
<widget class="QPushButton" name="setToCurrent">
<property name="text">
<string>Set to Current</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="8" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Tracks:</string>
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="QTableWidget" name="tracks">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="cornerButtonEnabled">
<bool>false</bool>
</property>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string>#</string>
</property>
</column>
<column>
<property name="text">
<string>Mode</string>
</property>
</column>
<column>
<property name="text">
<string>Start</string>
</property>
</column>
<column>
<property name="text">
<string>Length</string>
</property>
</column>
<column>
<property name="text">
<string>Hash</string>
</property>
</column>
</widget>
</item>
<item row="10" column="0" colspan="2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<spacer name="horizontalSpacer">

View file

@ -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<std::recursive_mutex> 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())
{

View file

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

View file

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

View file

@ -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<FrontendCommon::SaveStateSelectorUI>(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)

View file

@ -266,6 +266,8 @@ protected:
void RecreateSystem() override;
void ApplyGameSettings(bool display_osd_messages);
virtual void DrawImGuiWindows();
void DrawFPSWindow();