Cheats: Rewrite cheat management system

This commit is contained in:
Stenzek 2024-10-07 01:44:13 +10:00
parent 1acc3999d8
commit 2f0a510108
No known key found for this signature in database
53 changed files with 4997 additions and 3177 deletions

View File

@ -85,6 +85,8 @@ add_library(core
memory_card.h
memory_card_image.cpp
memory_card_image.h
memory_scanner.cpp
memory_scanner.h
mips_encoder.h
multitap.cpp
multitap.h
@ -136,7 +138,7 @@ target_precompile_headers(core PRIVATE "pch.h")
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 util)
target_link_libraries(core PRIVATE xxhash imgui rapidyaml rcheevos cpuinfo::cpuinfo ZLIB::ZLIB Zstd::Zstd)
target_link_libraries(core PRIVATE xxhash imgui minizip rapidyaml rcheevos cpuinfo::cpuinfo ZLIB::ZLIB Zstd::Zstd)
if(CPU_ARCH_X64)
target_compile_definitions(core PUBLIC "ENABLE_RECOMPILER=1" "ENABLE_NEWREC=1" "ENABLE_MMAP_FASTMEM=1")

File diff suppressed because it is too large Load Diff

View File

@ -7,316 +7,145 @@
#include "types.h"
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
struct CheatCode
class Error;
namespace Cheats {
enum class CodeType : u8
{
enum class Type : u8
{
Gameshark,
Count
};
Gameshark,
Count
};
enum class Activation : u8
{
Manual,
EndFrame,
Count,
};
enum class CodeActivation : u8
{
Manual,
EndFrame,
Count,
};
enum class InstructionCode : u8
{
Nop = 0x00,
ConstantWrite8 = 0x30,
ConstantWrite16 = 0x80,
ScratchpadWrite16 = 0x1F,
Increment16 = 0x10,
Decrement16 = 0x11,
Increment8 = 0x20,
Decrement8 = 0x21,
DelayActivation = 0xC1,
SkipIfNotEqual16 = 0xC0,
SkipIfButtonsNotEqual = 0xD5,
SkipIfButtonsEqual = 0xD6,
CompareButtons = 0xD4,
CompareEqual16 = 0xD0,
CompareNotEqual16 = 0xD1,
CompareLess16 = 0xD2,
CompareGreater16 = 0xD3,
CompareEqual8 = 0xE0,
CompareNotEqual8 = 0xE1,
CompareLess8 = 0xE2,
CompareGreater8 = 0xE3,
Slide = 0x50,
MemoryCopy = 0xC2,
ExtImprovedSlide = 0x53,
enum class FileFormat : u8
{
Unknown,
PCSX,
Libretro,
EPSXe,
Count
};
// Extension opcodes, not present on original GameShark.
ExtConstantWrite32 = 0x90,
ExtScratchpadWrite32 = 0xA5,
ExtCompareEqual32 = 0xA0,
ExtCompareNotEqual32 = 0xA1,
ExtCompareLess32 = 0xA2,
ExtCompareGreater32 = 0xA3,
ExtSkipIfNotEqual32 = 0xA4,
ExtIncrement32 = 0x60,
ExtDecrement32 = 0x61,
ExtConstantWriteIfMatch16 = 0xA6,
ExtConstantWriteIfMatchWithRestore16 = 0xA7,
ExtConstantForceRange8 = 0xF0,
ExtConstantForceRangeLimits16 = 0xF1,
ExtConstantForceRangeRollRound16 = 0xF2,
ExtConstantForceRange16 = 0xF3,
ExtFindAndReplace = 0xF4,
ExtConstantSwap16 = 0xF5,
using CodeOption = std::pair<std::string, u32>;
using CodeOptionList = std::vector<CodeOption>;
ExtConstantBitSet8 = 0x31,
ExtConstantBitClear8 = 0x32,
ExtConstantBitSet16 = 0x81,
ExtConstantBitClear16 = 0x82,
ExtConstantBitSet32 = 0x91,
ExtConstantBitClear32 = 0x92,
ExtBitCompareButtons = 0xD7,
ExtSkipIfNotLess8 = 0xC3,
ExtSkipIfNotGreater8 = 0xC4,
ExtSkipIfNotLess16 = 0xC5,
ExtSkipIfNotGreater16 = 0xC6,
ExtMultiConditionals = 0xF6,
ExtCheatRegisters = 0x51,
ExtCheatRegistersCompare = 0x52,
ExtCompareBitsSet8 = 0xE4, //Only used inside ExtMultiConditionals
ExtCompareBitsClear8 = 0xE5, //Only used inside ExtMultiConditionals
};
union Instruction
{
u64 bits;
struct
{
u32 second;
u32 first;
};
BitField<u64, InstructionCode, 32 + 24, 8> code;
BitField<u64, u32, 32, 24> address;
BitField<u64, u32, 0, 32> value32;
BitField<u64, u16, 0, 16> value16;
BitField<u64, u8, 0, 8> value8;
};
std::string group;
/// Contains all the information required to present a cheat code to the user.
struct CodeInfo
{
std::string name;
std::string author;
std::string description;
std::vector<Instruction> instructions;
std::string comments;
Type type = Type::Gameshark;
Activation activation = Activation::EndFrame;
bool enabled = false;
std::string body;
CodeOptionList options;
u16 option_range_start = 0;
u16 option_range_end = 0;
u32 file_offset_start = 0;
u32 file_offset_body_start = 0;
u32 file_offset_end = 0;
CodeType type = CodeType::Gameshark;
CodeActivation activation = CodeActivation::EndFrame;
bool from_database = false;
ALWAYS_INLINE bool Valid() const { return !instructions.empty() && !description.empty(); }
ALWAYS_INLINE bool IsManuallyActivated() const { return (activation == Activation::Manual); }
std::string_view GetNamePart() const;
std::string_view GetNameParentPart() const;
std::string GetInstructionsAsString() const;
bool SetInstructionsFromString(const std::string& str);
u32 GetNextNonConditionalInstruction(u32 index) const;
void Apply() const;
void ApplyOnDisable() const;
static const char* GetTypeName(Type type);
static const char* GetTypeDisplayName(Type type);
static std::optional<Type> ParseTypeName(const char* str);
static const char* GetActivationName(Activation activation);
static const char* GetActivationDisplayName(Activation activation);
static std::optional<Activation> ParseActivationName(const char* str);
bool HasOptionChoices() const { return (!options.empty()); }
bool HasOptionRange() const { return (option_range_end > option_range_start); }
std::string_view MapOptionValueToName(u32 value) const;
std::string_view MapOptionValueToName(const std::string_view value) const;
u32 MapOptionNameToValue(const std::string_view opt_name) const;
};
class CheatList final
{
public:
enum class Format
{
Autodetect,
PCSXR,
Libretro,
EPSXe,
Count
};
using CodeInfoList = std::vector<CodeInfo>;
CheatList();
~CheatList();
/// Returns the internal identifier for a code type.
extern const char* GetTypeName(CodeType type);
ALWAYS_INLINE const CheatCode& GetCode(u32 i) const { return m_codes[i]; }
ALWAYS_INLINE CheatCode& GetCode(u32 i) { return m_codes[i]; }
ALWAYS_INLINE u32 GetCodeCount() const { return static_cast<u32>(m_codes.size()); }
ALWAYS_INLINE bool IsCodeEnabled(u32 index) const { return m_codes[index].enabled; }
/// Returns the human-readable name for a code type.
extern const char* GetTypeDisplayName(CodeType type);
ALWAYS_INLINE bool GetMasterEnable() const { return m_master_enable; }
ALWAYS_INLINE void SetMasterEnable(bool enable) { m_master_enable = enable; }
/// Parses an internal identifier, returning the code type.
extern std::optional<CodeType> ParseTypeName(const std::string_view str);
const CheatCode* FindCode(const char* name) const;
const CheatCode* FindCode(const char* group, const char* name) const;
/// Returns the internal identifier for a code activation.
extern const char* GetActivationName(CodeActivation activation);
void AddCode(CheatCode cc);
void SetCode(u32 index, CheatCode cc);
void RemoveCode(u32 i);
/// Returns the human-readable name for a code activation.
extern const char* GetActivationDisplayName(CodeActivation activation);
u32 GetEnabledCodeCount() const;
std::vector<std::string> GetCodeGroups() const;
void EnableCode(u32 index);
void DisableCode(u32 index);
void SetCodeEnabled(u32 index, bool state);
/// Parses an internal identifier, returning the activation type.
extern std::optional<CodeActivation> ParseActivationName(const std::string_view str);
static std::optional<Format> DetectFileFormat(const char* filename);
static Format DetectFileFormat(const std::string& str);
static bool ParseLibretroCheat(CheatCode* cc, const char* line);
/// Returns a list of all available cheats/patches for a given game.
extern CodeInfoList GetCodeInfoList(const std::string_view serial, std::optional<GameHash> hash, bool cheats,
bool load_from_database, bool sort_by_name);
bool LoadFromFile(const char* filename, Format format);
bool LoadFromPCSXRFile(const char* filename);
bool LoadFromLibretroFile(const char* filename);
/// Returns a list of all unique prefixes/groups for a cheat list.
extern std::vector<std::string_view> GetCodeListUniquePrefixes(const CodeInfoList& list, bool include_empty);
bool LoadFromString(const std::string& str, Format format);
bool LoadFromPCSXRString(const std::string& str);
bool LoadFromLibretroString(const std::string& str);
bool LoadFromEPSXeString(const std::string& str);
/// Searches for a given code by name.
extern const CodeInfo* FindCodeInInfoList(const CodeInfoList& list, const std::string_view name);
bool SaveToPCSXRFile(const char* filename);
/// Searches for a given code by name.
extern CodeInfo* FindCodeInInfoList(CodeInfoList& list, const std::string_view name);
bool LoadFromPackage(const std::string& serial);
/// Imports all codes from the provided string.
extern bool ImportCodesFromString(CodeInfoList* dst, const std::string_view file_contents, FileFormat file_format,
bool stop_on_error, Error* error);
void Apply();
/// Exports codes to the given file, in DuckStation format.
extern bool ExportCodesToFile(std::string path, const CodeInfoList& codes, Error* error);
void ApplyCode(u32 index);
/// Adds, updates, or removes the specified code from the file, rewriting it. If code is null, it will be removed.
extern bool UpdateCodeInFile(const char* path, const std::string_view name, const CodeInfo* code, Error* error);
void MergeList(const CheatList& cl);
/// Updates or adds multiple codes to the file, rewriting it.
extern bool SaveCodesToFile(const char* path, const CodeInfoList& codes, Error* error);
private:
std::vector<CheatCode> m_codes;
bool m_master_enable = true;
};
/// Merges two cheat lists, with any duplicates in the new list taking precedence.
extern void MergeCheatList(CodeInfoList* dst, CodeInfoList src);
class MemoryScan
{
public:
enum class Operator
{
Any,
LessThanLast,
LessEqualLast,
GreaterThanLast,
GreaterEqualLast,
NotEqualLast,
EqualLast,
DecreasedBy,
IncreasedBy,
ChangedBy,
Equal,
NotEqual,
LessThan,
LessEqual,
GreaterThan,
GreaterEqual
};
/// Returns the path to a new cheat/patch cht for the specified serial and hash.
extern std::string GetChtFilename(const std::string_view serial, std::optional<GameHash> hash, bool cheats);
struct Result
{
PhysicalMemoryAddress address;
u32 value;
u32 last_value;
bool value_changed;
/// Reloads cheats and game patches. The parameters control the degree to which data is reloaded.
extern void ReloadCheats(bool reload_files, bool reload_enabled_list, bool verbose, bool verbose_if_changed);
bool Filter(Operator op, u32 comp_value, bool is_signed) const;
void UpdateValue(MemoryAccessSize size, bool is_signed);
};
/// Releases all cheat-related state.
extern void UnloadAll();
using ResultVector = std::vector<Result>;
/// Applies setting changes based on patches.
extern void ApplySettingOverrides();
MemoryScan();
~MemoryScan();
/// Applies all currently-registered frame end cheat codes.
extern void ApplyFrameEndCodes();
u32 GetValue() const { return m_value; }
bool GetValueSigned() const { return m_signed; }
MemoryAccessSize GetSize() const { return m_size; }
Operator GetOperator() const { return m_operator; }
PhysicalMemoryAddress GetStartAddress() const { return m_start_address; }
PhysicalMemoryAddress GetEndAddress() const { return m_end_address; }
const ResultVector& GetResults() const { return m_results; }
const Result& GetResult(u32 index) const { return m_results[index]; }
u32 GetResultCount() const { return static_cast<u32>(m_results.size()); }
/// Returns true if cheats are enabled in the current game's configuration.
extern bool AreCheatsEnabled();
void SetValue(u32 value) { m_value = value; }
void SetValueSigned(bool s) { m_signed = s; }
void SetSize(MemoryAccessSize size) { m_size = size; }
void SetOperator(Operator op) { m_operator = op; }
void SetStartAddress(PhysicalMemoryAddress addr) { m_start_address = addr; }
void SetEndAddress(PhysicalMemoryAddress addr) { m_end_address = addr; }
/// Enumerates the names of all manually-activated codes.
extern bool EnumerateManualCodes(std::function<bool(const std::string& name)> callback);
void ResetSearch();
void Search();
void SearchAgain();
void UpdateResultsValues();
/// Invokes/applies the specified manually-activated code.
extern bool ApplyManualCode(const std::string_view name);
void SetResultValue(u32 index, u32 value);
// Config sections/keys to use to enable patches.
extern const char* PATCHES_CONFIG_SECTION;
extern const char* CHEATS_CONFIG_SECTION;
extern const char* PATCH_ENABLE_CONFIG_KEY;
private:
void SearchBytes();
void SearchHalfwords();
void SearchWords();
u32 m_value = 0;
MemoryAccessSize m_size = MemoryAccessSize::HalfWord;
Operator m_operator = Operator::Equal;
PhysicalMemoryAddress m_start_address = 0;
PhysicalMemoryAddress m_end_address = 0x200000;
ResultVector m_results;
bool m_signed = false;
};
class MemoryWatchList
{
public:
MemoryWatchList();
~MemoryWatchList();
struct Entry
{
std::string description;
u32 address;
u32 value;
MemoryAccessSize size;
bool is_signed;
bool freeze;
bool changed;
};
using EntryVector = std::vector<Entry>;
const Entry* GetEntryByAddress(u32 address) const;
const EntryVector& GetEntries() const { return m_entries; }
const Entry& GetEntry(u32 index) const { return m_entries[index]; }
u32 GetEntryCount() const { return static_cast<u32>(m_entries.size()); }
bool AddEntry(std::string description, u32 address, MemoryAccessSize size, bool is_signed, bool freeze);
void RemoveEntry(u32 index);
bool RemoveEntryByDescription(const char* description);
bool RemoveEntryByAddress(u32 address);
void SetEntryDescription(u32 index, std::string description);
void SetEntryFreeze(u32 index, bool freeze);
void SetEntryValue(u32 index, u32 value);
void UpdateValues();
private:
static void SetEntryValue(Entry* entry, u32 value);
static void UpdateEntryValue(Entry* entry);
EntryVector m_entries;
};
} // namespace Cheats

View File

@ -65,6 +65,7 @@
<ClCompile Include="mdec.cpp" />
<ClCompile Include="memory_card.cpp" />
<ClCompile Include="memory_card_image.cpp" />
<ClCompile Include="memory_scanner.cpp" />
<ClCompile Include="multitap.cpp" />
<ClCompile Include="guncon.cpp" />
<ClCompile Include="negcon.cpp" />
@ -145,6 +146,7 @@
<ClInclude Include="mdec.h" />
<ClInclude Include="memory_card.h" />
<ClInclude Include="memory_card_image.h" />
<ClInclude Include="memory_scanner.h" />
<ClInclude Include="multitap.h" />
<ClInclude Include="guncon.h" />
<ClInclude Include="negcon.h" />
@ -170,6 +172,9 @@
<ProjectReference Include="..\..\dep\imgui\imgui.vcxproj">
<Project>{bb08260f-6fbc-46af-8924-090ee71360c6}</Project>
</ProjectReference>
<ProjectReference Include="..\..\dep\minizip\minizip.vcxproj">
<Project>{8bda439c-6358-45fb-9994-2ff083babe06}</Project>
</ProjectReference>
<ProjectReference Include="..\..\dep\rainterface\rainterface.vcxproj" Condition="'$(Platform)'!='ARM64'">
<Project>{e4357877-d459-45c7-b8f6-dcbb587bb528}</Project>
</ProjectReference>
@ -206,7 +211,8 @@
<ItemDefinitionGroup>
<ClCompile>
<PreprocessorDefinitions Condition="'$(Platform)'=='x64' And $(Configuration.Contains('Debug'))">ZYDIS_DISABLE_ENCODER;ZYDIS_DISABLE_AVX512;ZYDIS_DISABLE_KNC;ZYDIS_STATIC_BUILD;ZYCORE_STATIC_BUILD;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<AdditionalIncludeDirectories Condition="'$(Platform)'=='x64' And $(Configuration.Contains('Debug'))">$(SolutionDir)dep\zydis\include;$(SolutionDir)dep\zydis\dependencies\zycore\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories Condition="'$(Platform)'=='x64' And $(Configuration.Contains('Debug'))">%(AdditionalIncludeDirectories);$(SolutionDir)dep\zydis\include;$(SolutionDir)dep\zydis\dependencies\zycore\include</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>%(AdditionalIncludeDirectories);$(SolutionDir)dep\minizip\include</AdditionalIncludeDirectories>
<ObjectFileName>$(IntDir)/%(RelativeDir)/</ObjectFileName>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>

View File

@ -67,6 +67,7 @@
<ClCompile Include="gdb_server.cpp" />
<ClCompile Include="gpu_sw_rasterizer.cpp" />
<ClCompile Include="gpu_hw_texture_cache.cpp" />
<ClCompile Include="memory_scanner.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="types.h" />
@ -140,6 +141,7 @@
<ClInclude Include="gdb_server.h" />
<ClInclude Include="gpu_sw_rasterizer.h" />
<ClInclude Include="gpu_hw_texture_cache.h" />
<ClInclude Include="memory_scanner.h" />
</ItemGroup>
<ItemGroup>
<None Include="gpu_sw_rasterizer.inl" />

View File

@ -197,6 +197,8 @@ enum class SettingsPage
Audio,
Achievements,
Advanced,
Patches,
Cheats,
Count
};
@ -276,7 +278,6 @@ static void DoChangeDisc();
static void DoRequestExit();
static void DoDesktopMode();
static void DoToggleFullscreen();
static void DoCheatsMenu();
static void DoToggleAnalogMode();
//////////////////////////////////////////////////////////////////////////
@ -304,6 +305,7 @@ static void DrawControllerSettingsPage();
static void DrawHotkeySettingsPage();
static void DrawAchievementsSettingsPage();
static void DrawAdvancedSettingsPage();
static void DrawPatchesOrCheatsSettingsPage(bool cheats);
static bool IsEditingGameSettings(SettingsInterface* bsi);
static SettingsInterface* GetEditingSettingsInterface();
@ -386,6 +388,7 @@ static void DrawFolderSetting(SettingsInterface* bsi, const char* title, const c
static void PopulateGraphicsAdapterList();
static void PopulateGameListDirectoryCache(SettingsInterface* si);
static void PopulatePatchesAndCheatsList(const std::string_view serial);
static void PopulatePostProcessingChain(SettingsInterface* si, const char* section);
static void BeginInputBinding(SettingsInterface* bsi, InputBindingInfo::Type type, std::string_view section,
std::string_view key, std::string_view display_name);
@ -402,6 +405,11 @@ static std::unique_ptr<GameList::Entry> s_game_settings_entry;
static std::vector<std::pair<std::string, bool>> s_game_list_directories_cache;
static GPUDevice::AdapterInfoList s_graphics_adapter_list_cache;
static std::vector<std::string> s_fullscreen_mode_list_cache;
static Cheats::CodeInfoList s_game_patch_list;
static std::vector<std::string> s_enabled_game_patch_cache;
static Cheats::CodeInfoList s_game_cheats_list;
static std::vector<std::string> s_enabled_game_cheat_cache;
static std::vector<std::string_view> s_game_cheat_groups;
static std::vector<PostProcessingStageInfo> s_postprocessing_stages;
static std::vector<const HotkeyInfo*> s_hotkey_list_cache;
static std::atomic_bool s_settings_changed{false};
@ -754,6 +762,11 @@ void FullscreenUI::Shutdown()
std::memset(s_controller_macro_expanded, 0, sizeof(s_controller_macro_expanded));
s_game_list_sorted_entries = {};
s_game_list_directories_cache = {};
s_game_patch_list = {};
s_enabled_game_patch_cache = {};
s_game_cheats_list = {};
s_enabled_game_cheat_cache = {};
s_game_cheat_groups = {};
s_postprocessing_stages = {};
s_fullscreen_mode_list_cache = {};
s_graphics_adapter_list_cache = {};
@ -1211,48 +1224,6 @@ void FullscreenUI::DoChangeDisc()
DoChangeDiscFromFile();
}
void FullscreenUI::DoCheatsMenu()
{
CheatList* cl = System::GetCheatList();
if (!cl)
{
if (!System::LoadCheatListFromDatabase() || ((cl = System::GetCheatList()) == nullptr))
{
Host::AddKeyedOSDMessage("load_cheat_list",
fmt::format(FSUI_FSTR("No cheats found for {}."), System::GetGameTitle()), 10.0f);
ReturnToPreviousWindow();
return;
}
}
ImGuiFullscreen::ChoiceDialogOptions options;
options.reserve(cl->GetCodeCount());
for (u32 i = 0; i < cl->GetCodeCount(); i++)
{
const CheatCode& cc = cl->GetCode(i);
options.emplace_back(cc.description.c_str(), cc.enabled);
}
auto callback = [](s32 index, const std::string& title, bool checked) {
if (index < 0)
{
ReturnToPreviousWindow();
return;
}
CheatList* cl = System::GetCheatList();
if (!cl)
return;
const CheatCode& cc = cl->GetCode(static_cast<u32>(index));
if (cc.activation == CheatCode::Activation::Manual)
cl->ApplyCode(static_cast<u32>(index));
else
System::SetCheatCodeState(static_cast<u32>(index), checked);
};
OpenChoiceDialog(FSUI_ICONSTR(ICON_FA_FROWN, "Cheat List"), true, std::move(options), std::move(callback));
}
void FullscreenUI::DoToggleAnalogMode()
{
// hacky way to toggle analog mode
@ -2728,6 +2699,11 @@ void FullscreenUI::SwitchToSettings()
{
s_game_settings_entry.reset();
s_game_settings_interface.reset();
s_game_patch_list = {};
s_enabled_game_patch_cache = {};
s_game_cheats_list = {};
s_enabled_game_cheat_cache = {};
s_game_cheat_groups = {};
PopulateGraphicsAdapterList();
PopulatePostProcessingChain(GetEditingSettingsInterface(), PostProcessing::Config::DISPLAY_CHAIN_SECTION);
@ -2741,6 +2717,7 @@ void FullscreenUI::SwitchToGameSettingsForSerial(std::string_view serial)
s_game_settings_entry.reset();
s_game_settings_interface = std::make_unique<INISettingsInterface>(System::GetGameSettingsPath(serial));
s_game_settings_interface->Load();
PopulatePatchesAndCheatsList(serial);
s_current_main_window = MainWindowType::Settings;
s_settings_page = SettingsPage::Summary;
QueueResetFocus(FocusResetType::ViewChanged);
@ -2796,6 +2773,19 @@ void FullscreenUI::PopulateGameListDirectoryCache(SettingsInterface* si)
s_game_list_directories_cache.emplace_back(std::move(dir), true);
}
void FullscreenUI::PopulatePatchesAndCheatsList(const std::string_view serial)
{
s_game_patch_list = Cheats::GetCodeInfoList(serial, std::nullopt, false, true, true);
s_game_cheats_list =
Cheats::GetCodeInfoList(serial, std::nullopt, true,
s_game_settings_interface->GetBoolValue("Cheats", "LoadCheatsFromDatabase", true), true);
s_game_cheat_groups = Cheats::GetCodeListUniquePrefixes(s_game_cheats_list, true);
s_enabled_game_patch_cache =
s_game_settings_interface->GetStringList(Cheats::PATCHES_CONFIG_SECTION, Cheats::PATCH_ENABLE_CONFIG_KEY);
s_enabled_game_cheat_cache =
s_game_settings_interface->GetStringList(Cheats::CHEATS_CONFIG_SECTION, Cheats::PATCH_ENABLE_CONFIG_KEY);
}
void FullscreenUI::DoCopyGameSettings()
{
if (!s_game_settings_interface)
@ -2839,32 +2829,34 @@ void FullscreenUI::DrawSettingsWindow()
{
static constexpr float ITEM_WIDTH = 25.0f;
static constexpr const char* global_icons[] = {
ICON_FA_TV, ICON_FA_DICE_D20, ICON_FA_COGS, ICON_PF_MICROCHIP,
ICON_PF_PICTURE, ICON_FA_MAGIC, ICON_PF_SOUND, ICON_PF_GAMEPAD_ALT,
ICON_PF_KEYBOARD_ALT, ICON_PF_MEMORY_CARD, ICON_FA_TROPHY, ICON_FA_EXCLAMATION_TRIANGLE};
static constexpr const char* per_game_icons[] = {ICON_FA_PARAGRAPH, ICON_FA_HDD, ICON_FA_COGS,
ICON_PF_PICTURE, ICON_PF_SOUND, ICON_PF_GAMEPAD_ALT,
ICON_PF_MEMORY_CARD, ICON_FA_TROPHY, ICON_FA_EXCLAMATION_TRIANGLE};
static constexpr SettingsPage global_pages[] = {
static constexpr const SettingsPage global_pages[] = {
SettingsPage::Interface, SettingsPage::Console, SettingsPage::Emulation, SettingsPage::BIOS,
SettingsPage::Display, SettingsPage::PostProcessing, SettingsPage::Audio, SettingsPage::Controller,
SettingsPage::Hotkey, SettingsPage::MemoryCards, SettingsPage::Achievements, SettingsPage::Advanced};
static constexpr SettingsPage per_game_pages[] = {
SettingsPage::Summary, SettingsPage::Console, SettingsPage::Emulation,
SettingsPage::Display, SettingsPage::Audio, SettingsPage::Controller,
static constexpr const SettingsPage per_game_pages[] = {
SettingsPage::Summary, SettingsPage::Console, SettingsPage::Emulation, SettingsPage::Patches,
SettingsPage::Cheats, SettingsPage::Display, SettingsPage::Audio, SettingsPage::Controller,
SettingsPage::MemoryCards, SettingsPage::Achievements, SettingsPage::Advanced};
static constexpr std::array<const char*, static_cast<u32>(SettingsPage::Count)> titles = {
{FSUI_NSTR("Summary"), FSUI_NSTR("Interface Settings"), FSUI_NSTR("Console Settings"),
FSUI_NSTR("Emulation Settings"), FSUI_NSTR("BIOS Settings"), FSUI_NSTR("Controller Settings"),
FSUI_NSTR("Hotkey Settings"), FSUI_NSTR("Memory Card Settings"), FSUI_NSTR("Graphics Settings"),
FSUI_NSTR("Post-Processing Settings"), FSUI_NSTR("Audio Settings"), FSUI_NSTR("Achievements Settings"),
FSUI_NSTR("Advanced Settings")}};
static constexpr std::array<std::pair<const char*, const char*>, static_cast<u32>(SettingsPage::Count)> titles = {
{{FSUI_NSTR("Summary"), ICON_FA_PARAGRAPH},
{FSUI_NSTR("Interface Settings"), ICON_FA_TV},
{FSUI_NSTR("Console Settings"), ICON_FA_DICE_D20},
{FSUI_NSTR("Emulation Settings"), ICON_FA_COGS},
{FSUI_NSTR("BIOS Settings"), ICON_PF_MICROCHIP},
{FSUI_NSTR("Controller Settings"), ICON_PF_GAMEPAD_ALT},
{FSUI_NSTR("Hotkey Settings"), ICON_PF_KEYBOARD_ALT},
{FSUI_NSTR("Memory Card Settings"), ICON_PF_MEMORY_CARD},
{FSUI_NSTR("Graphics Settings"), ICON_PF_PICTURE},
{FSUI_NSTR("Post-Processing Settings"), ICON_FA_MAGIC},
{FSUI_NSTR("Audio Settings"), ICON_PF_SOUND},
{FSUI_NSTR("Achievements Settings"), ICON_FA_TROPHY},
{FSUI_NSTR("Advanced Settings"), ICON_FA_EXCLAMATION_TRIANGLE},
{FSUI_NSTR("Patches"), ICON_FA_BAND_AID},
{FSUI_NSTR("Cheats"), ICON_FA_FLASK}}};
const bool game_settings = IsEditingGameSettings(GetEditingSettingsInterface());
const u32 count =
game_settings ? static_cast<u32>(std::size(per_game_pages)) : static_cast<u32>(std::size(global_pages));
const char* const* icons = game_settings ? per_game_icons : global_icons;
const SettingsPage* pages = game_settings ? per_game_pages : global_pages;
u32 index = 0;
for (u32 i = 0; i < count; i++)
@ -2903,13 +2895,14 @@ void FullscreenUI::DrawSettingsWindow()
if (s_game_settings_entry)
NavTitle(s_game_settings_entry->title.c_str());
else
NavTitle(Host::TranslateToCString(TR_CONTEXT, titles[static_cast<u32>(pages[index])]));
NavTitle(Host::TranslateToCString(TR_CONTEXT, titles[static_cast<u32>(pages[index])].first));
RightAlignNavButtons(count, ITEM_WIDTH, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY);
for (u32 i = 0; i < count; i++)
{
if (NavButton(icons[i], i == index, true, ITEM_WIDTH, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY))
if (NavButton(titles[static_cast<u32>(pages[i])].second, i == index, true, ITEM_WIDTH,
LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY))
{
s_settings_page = pages[i];
QueueResetFocus(FocusResetType::Other);
@ -2994,6 +2987,14 @@ void FullscreenUI::DrawSettingsWindow()
DrawAdvancedSettingsPage();
break;
case SettingsPage::Patches:
DrawPatchesOrCheatsSettingsPage(false);
break;
case SettingsPage::Cheats:
DrawPatchesOrCheatsSettingsPage(true);
break;
default:
break;
}
@ -5220,6 +5221,186 @@ void FullscreenUI::DrawAdvancedSettingsPage()
EndMenuButtons();
}
void FullscreenUI::DrawPatchesOrCheatsSettingsPage(bool cheats)
{
SettingsInterface* bsi = GetEditingSettingsInterface();
const Cheats::CodeInfoList& code_list = cheats ? s_game_cheats_list : s_game_patch_list;
std::vector<std::string>& enable_list = cheats ? s_enabled_game_cheat_cache : s_enabled_game_patch_cache;
const char* section = cheats ? Cheats::CHEATS_CONFIG_SECTION : Cheats::PATCHES_CONFIG_SECTION;
BeginMenuButtons();
static constexpr auto draw_code = [](SettingsInterface* bsi, const char* section, const Cheats::CodeInfo& ci,
std::vector<std::string>& enable_list, bool cheats) {
const auto enable_it = std::find(enable_list.begin(), enable_list.end(), ci.name);
SmallString title;
if (!cheats)
title = std::string_view(ci.name);
else
title = ci.GetNamePart();
// TODO: Handle ranges.
bool state = (enable_it != enable_list.end());
if (ci.HasOptionChoices())
{
TinyString visible_value(FSUI_VSTR("Disabled"));
bool has_option = false;
if (state)
{
// Need to map the value to an option.
visible_value = ci.MapOptionValueToName(bsi->GetTinyStringValue(section, ci.name.c_str()));
has_option = true;
}
if (MenuButtonWithValue(title.c_str(), ci.description.c_str(), visible_value))
{
ImGuiFullscreen::ChoiceDialogOptions options;
options.reserve(ci.options.size() + 1);
options.emplace_back(FSUI_VSTR("Disabled"), !has_option);
for (const Cheats::CodeOption& opt : ci.options)
options.emplace_back(opt.first, has_option && (visible_value.view() == opt.first));
OpenChoiceDialog(ci.name, false, std::move(options),
[cheat_name = ci.name, cheats, section](s32 index, const std::string& title, bool checked) {
if (index >= 0)
{
const Cheats::CodeInfo* ci =
Cheats::FindCodeInInfoList(cheats ? s_game_cheats_list : s_game_patch_list, cheat_name);
if (ci)
{
SettingsInterface* bsi = GetEditingSettingsInterface();
std::vector<std::string>& enable_list =
cheats ? s_enabled_game_cheat_cache : s_enabled_game_patch_cache;
const auto it = std::find(enable_list.begin(), enable_list.end(), ci->name);
if (index == 0)
{
bsi->RemoveFromStringList(section, Cheats::PATCH_ENABLE_CONFIG_KEY, ci->name.c_str());
if (it != enable_list.end())
enable_list.erase(it);
}
else
{
bsi->AddToStringList(section, Cheats::PATCH_ENABLE_CONFIG_KEY, ci->name.c_str());
bsi->SetUIntValue(section, ci->name.c_str(), ci->MapOptionNameToValue(title));
if (it == enable_list.end())
enable_list.push_back(std::move(cheat_name));
}
SetSettingsChanged(bsi);
}
}
CloseChoiceDialog();
});
}
}
else
{
const bool changed = ToggleButton(title.c_str(), ci.description.c_str(), &state);
if (changed)
{
if (state)
{
bsi->AddToStringList(section, Cheats::PATCH_ENABLE_CONFIG_KEY, ci.name.c_str());
enable_list.push_back(ci.name);
}
else
{
bsi->RemoveFromStringList(section, Cheats::PATCH_ENABLE_CONFIG_KEY, ci.name.c_str());
enable_list.erase(enable_it);
}
SetSettingsChanged(bsi);
}
}
};
if (cheats)
{
ActiveButton(
FSUI_CSTR(
"WARNING: Activating cheats can cause unpredictable behavior, crashing, soft-locks, or broken saved games."),
false, false, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY);
MenuHeading(FSUI_CSTR("Settings"));
bool enable_cheats = bsi->GetBoolValue("Cheats", "EnableCheats", false);
if (ToggleButton(FSUI_CSTR("Enable Cheats"), FSUI_CSTR("Enables the cheats that are selected below."),
&enable_cheats))
{
if (enable_cheats)
bsi->SetBoolValue("Cheats", "EnableCheats", true);
else
bsi->DeleteValue("Cheats", "EnableCheats");
SetSettingsChanged(bsi);
}
bool load_database_cheats = bsi->GetBoolValue("Cheats", "LoadCheatsFromDatabase", true);
if (ToggleButton(FSUI_CSTR("Load Database Cheats"),
FSUI_CSTR("Enables loading of cheats for this game from DuckStation's database."),
&load_database_cheats))
{
if (load_database_cheats)
bsi->DeleteValue("Cheats", "LoadCheatsFromDatabase");
else
bsi->SetBoolValue("Cheats", "LoadCheatsFromDatabase", false);
SetSettingsChanged(bsi);
if (s_game_settings_entry)
PopulatePatchesAndCheatsList(s_game_settings_entry->serial);
}
if (code_list.empty())
{
ActiveButton(FSUI_CSTR("No cheats are available for this game."), false, false,
ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY);
}
else
{
for (const std::string_view& group : s_game_cheat_groups)
{
if (group.empty())
MenuHeading(FSUI_CSTR("Ungrouped"));
else
MenuHeading(SmallString(group).c_str());
for (const Cheats::CodeInfo& ci : code_list)
{
if (ci.GetNameParentPart() != group)
continue;
draw_code(bsi, section, ci, enable_list, true);
}
}
}
}
else
{
ActiveButton(
FSUI_CSTR("WARNING: Activating game patches can cause unpredictable behavior, crashing, soft-locks, or broken "
"saved games."),
false, false, ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY);
if (code_list.empty())
{
ActiveButton(FSUI_CSTR("No patches are available for this game."), false, false,
ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY);
}
else
{
MenuHeading(FSUI_CSTR("Game Patches"));
for (const Cheats::CodeInfo& ci : code_list)
draw_code(bsi, section, ci, enable_list, false);
}
}
EndMenuButtons();
}
void FullscreenUI::DrawPauseMenu()
{
SmallString buffer;
@ -5327,7 +5508,7 @@ void FullscreenUI::DrawPauseMenu()
ImVec2(10.0f, 10.0f), ImGuiWindowFlags_NoBackground))
{
static constexpr u32 submenu_item_count[] = {
12, // None
11, // None
4, // Exit
3, // Achievements
};
@ -5365,13 +5546,6 @@ void FullscreenUI::DrawPauseMenu()
s_current_main_window = MainWindowType::None;
}
if (ActiveButton(FSUI_ICONSTR(ICON_FA_FROWN_OPEN, "Cheat List"), false,
!System::GetGameSerial().empty() && g_settings.enable_cheats))
{
s_current_main_window = MainWindowType::None;
DoCheatsMenu();
}
if (ActiveButton(FSUI_ICONSTR(ICON_FA_GAMEPAD, "Toggle Analog"), false))
{
ClosePauseMenu();

View File

@ -220,7 +220,7 @@ std::string GameDatabase::GetSerialForPath(const char* path)
const GameDatabase::Entry* GameDatabase::GetEntryForDisc(CDImage* image)
{
std::string id;
System::GameHash hash;
GameHash hash;
System::GetGameDetailsFromImage(image, &id, &hash);
const Entry* entry = GetEntryForGameDetails(id, hash);
if (entry)

View File

@ -82,7 +82,7 @@ struct MemcardTimestampCacheEntry
using CacheMap = PreferUnorderedStringMap<Entry>;
using PlayedTimeMap = PreferUnorderedStringMap<PlayedTimeEntry>;
static_assert(std::is_same_v<decltype(Entry::hash), System::GameHash>);
static_assert(std::is_same_v<decltype(Entry::hash), GameHash>);
static bool GetExeListEntry(const std::string& path, Entry* entry);
static bool GetPsfListEntry(const std::string& path, Entry* entry);
@ -193,7 +193,7 @@ bool GameList::GetExeListEntry(const std::string& path, GameList::Entry* entry)
return false;
}
const System::GameHash hash = System::GetGameHashFromFile(path.c_str());
const GameHash hash = System::GetGameHashFromFile(path.c_str());
entry->serial = hash ? System::GetGameHashId(hash) : std::string();
entry->title = Path::GetFileTitle(FileSystem::GetDisplayNameFromPath(path));

View File

@ -285,20 +285,6 @@ DEFINE_HOTKEY("Rewind", TRANSLATE_NOOP("Hotkeys", "System"), TRANSLATE_NOOP("Hot
System::SetRewindState(pressed > 0);
})
#ifndef __ANDROID__
DEFINE_HOTKEY("ToggleCheats", TRANSLATE_NOOP("Hotkeys", "System"), TRANSLATE_NOOP("Hotkeys", "Toggle Cheats"),
[](s32 pressed) {
if (!pressed)
System::DoToggleCheats();
})
#else
DEFINE_HOTKEY("TogglePatchCodes", TRANSLATE_NOOP("Hotkeys", "System"), TRANSLATE_NOOP("Hotkeys", "Toggle Patch Codes"),
[](s32 pressed) {
if (!pressed)
System::DoToggleCheats();
})
#endif
DEFINE_HOTKEY("ToggleOverclocking", TRANSLATE_NOOP("Hotkeys", "System"),
TRANSLATE_NOOP("Hotkeys", "Toggle Clock Speed Control (Overclocking)"), [](s32 pressed) {
if (!pressed && System::IsValid())

473
src/core/memory_scanner.cpp Normal file
View File

@ -0,0 +1,473 @@
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com> and contributors.
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#include "memory_scanner.h"
#include "bus.h"
#include "cpu_core.h"
#include "common/log.h"
#include "fmt/format.h"
LOG_CHANNEL(Cheats);
static bool IsValidScanAddress(PhysicalMemoryAddress address)
{
if ((address & CPU::SCRATCHPAD_ADDR_MASK) == CPU::SCRATCHPAD_ADDR &&
(address & CPU::SCRATCHPAD_OFFSET_MASK) < CPU::SCRATCHPAD_SIZE)
{
return true;
}
address &= CPU::PHYSICAL_MEMORY_ADDRESS_MASK;
if (address < Bus::RAM_MIRROR_END)
return true;
if (address >= Bus::BIOS_BASE && address < (Bus::BIOS_BASE + Bus::BIOS_SIZE))
return true;
return false;
}
MemoryScan::MemoryScan() = default;
MemoryScan::~MemoryScan() = default;
void MemoryScan::ResetSearch()
{
m_results.clear();
}
void MemoryScan::Search()
{
m_results.clear();
switch (m_size)
{
case MemoryAccessSize::Byte:
SearchBytes();
break;
case MemoryAccessSize::HalfWord:
SearchHalfwords();
break;
case MemoryAccessSize::Word:
SearchWords();
break;
default:
break;
}
}
void MemoryScan::SearchBytes()
{
for (PhysicalMemoryAddress address = m_start_address; address < m_end_address; address++)
{
if (!IsValidScanAddress(address))
continue;
u8 bvalue = 0;
if (!CPU::SafeReadMemoryByte(address, &bvalue)) [[unlikely]]
continue;
Result res;
res.address = address;
res.value = m_signed ? SignExtend32(bvalue) : ZeroExtend32(bvalue);
res.last_value = res.value;
res.value_changed = false;
if (res.Filter(m_operator, m_value, m_signed))
m_results.push_back(res);
}
}
void MemoryScan::SearchHalfwords()
{
for (PhysicalMemoryAddress address = m_start_address; address < m_end_address; address += 2)
{
if (!IsValidScanAddress(address))
continue;
u16 bvalue = 0;
if (!CPU::SafeReadMemoryHalfWord(address, &bvalue)) [[unlikely]]
continue;
Result res;
res.address = address;
res.value = m_signed ? SignExtend32(bvalue) : ZeroExtend32(bvalue);
res.last_value = res.value;
res.value_changed = false;
if (res.Filter(m_operator, m_value, m_signed))
m_results.push_back(res);
}
}
void MemoryScan::SearchWords()
{
for (PhysicalMemoryAddress address = m_start_address; address < m_end_address; address += 4)
{
if (!IsValidScanAddress(address))
continue;
u32 bvalue = 0;
if (!CPU::SafeReadMemoryWord(address, &bvalue)) [[unlikely]]
continue;
Result res;
res.address = address;
res.value = bvalue;
res.last_value = res.value;
res.value_changed = false;
if (res.Filter(m_operator, m_value, m_signed))
m_results.push_back(res);
}
}
void MemoryScan::SearchAgain()
{
ResultVector new_results;
new_results.reserve(m_results.size());
for (Result& res : m_results)
{
res.UpdateValue(m_size, m_signed);
if (res.Filter(m_operator, m_value, m_signed))
{
res.last_value = res.value;
new_results.push_back(res);
}
}
m_results.swap(new_results);
}
void MemoryScan::UpdateResultsValues()
{
for (Result& res : m_results)
res.UpdateValue(m_size, m_signed);
}
void MemoryScan::SetResultValue(u32 index, u32 value)
{
if (index >= m_results.size())
return;
Result& res = m_results[index];
if (res.value == value)
return;
switch (m_size)
{
case MemoryAccessSize::Byte:
CPU::SafeWriteMemoryByte(res.address, Truncate8(value));
break;
case MemoryAccessSize::HalfWord:
CPU::SafeWriteMemoryHalfWord(res.address, Truncate16(value));
break;
case MemoryAccessSize::Word:
CPU::SafeWriteMemoryWord(res.address, value);
break;
}
res.value = value;
res.value_changed = true;
}
bool MemoryScan::Result::Filter(Operator op, u32 comp_value, bool is_signed) const
{
switch (op)
{
case Operator::Equal:
{
return (value == comp_value);
}
case Operator::NotEqual:
{
return (value != comp_value);
}
case Operator::GreaterThan:
{
return is_signed ? (static_cast<s32>(value) > static_cast<s32>(comp_value)) : (value > comp_value);
}
case Operator::GreaterEqual:
{
return is_signed ? (static_cast<s32>(value) >= static_cast<s32>(comp_value)) : (value >= comp_value);
}
case Operator::LessThan:
{
return is_signed ? (static_cast<s32>(value) < static_cast<s32>(comp_value)) : (value < comp_value);
}
case Operator::LessEqual:
{
return is_signed ? (static_cast<s32>(value) <= static_cast<s32>(comp_value)) : (value <= comp_value);
}
case Operator::IncreasedBy:
{
return is_signed ? ((static_cast<s32>(value) - static_cast<s32>(last_value)) == static_cast<s32>(comp_value)) :
((value - last_value) == comp_value);
}
case Operator::DecreasedBy:
{
return is_signed ? ((static_cast<s32>(last_value) - static_cast<s32>(value)) == static_cast<s32>(comp_value)) :
((last_value - value) == comp_value);
}
case Operator::ChangedBy:
{
if (is_signed)
return (std::abs(static_cast<s32>(last_value) - static_cast<s32>(value)) == static_cast<s32>(comp_value));
else
return ((last_value > value) ? (last_value - value) : (value - last_value)) == comp_value;
}
case Operator::EqualLast:
{
return (value == last_value);
}
case Operator::NotEqualLast:
{
return (value != last_value);
}
case Operator::GreaterThanLast:
{
return is_signed ? (static_cast<s32>(value) > static_cast<s32>(last_value)) : (value > last_value);
}
case Operator::GreaterEqualLast:
{
return is_signed ? (static_cast<s32>(value) >= static_cast<s32>(last_value)) : (value >= last_value);
}
case Operator::LessThanLast:
{
return is_signed ? (static_cast<s32>(value) < static_cast<s32>(last_value)) : (value < last_value);
}
case Operator::LessEqualLast:
{
return is_signed ? (static_cast<s32>(value) <= static_cast<s32>(last_value)) : (value <= last_value);
}
case Operator::Any:
return true;
default:
return false;
}
}
void MemoryScan::Result::UpdateValue(MemoryAccessSize size, bool is_signed)
{
const u32 old_value = value;
switch (size)
{
case MemoryAccessSize::Byte:
{
u8 bvalue = 0;
if (CPU::SafeReadMemoryByte(address, &bvalue)) [[likely]]
value = is_signed ? SignExtend32(bvalue) : ZeroExtend32(bvalue);
}
break;
case MemoryAccessSize::HalfWord:
{
u16 bvalue = 0;
if (CPU::SafeReadMemoryHalfWord(address, &bvalue)) [[likely]]
value = is_signed ? SignExtend32(bvalue) : ZeroExtend32(bvalue);
}
break;
case MemoryAccessSize::Word:
{
CPU::SafeReadMemoryWord(address, &value);
}
break;
}
value_changed = (value != old_value);
}
MemoryWatchList::MemoryWatchList() = default;
MemoryWatchList::~MemoryWatchList() = default;
const MemoryWatchList::Entry* MemoryWatchList::GetEntryByAddress(u32 address) const
{
for (const Entry& entry : m_entries)
{
if (entry.address == address)
return &entry;
}
return nullptr;
}
bool MemoryWatchList::AddEntry(std::string description, u32 address, MemoryAccessSize size, bool is_signed, bool freeze)
{
if (GetEntryByAddress(address))
return false;
Entry entry;
entry.description = std::move(description);
entry.address = address;
entry.size = size;
entry.is_signed = is_signed;
entry.freeze = false;
UpdateEntryValue(&entry);
entry.changed = false;
entry.freeze = freeze;
m_entries.push_back(std::move(entry));
return true;
}
void MemoryWatchList::RemoveEntry(u32 index)
{
if (index >= m_entries.size())
return;
m_entries.erase(m_entries.begin() + index);
}
bool MemoryWatchList::RemoveEntryByAddress(u32 address)
{
for (auto it = m_entries.begin(); it != m_entries.end(); ++it)
{
if (it->address == address)
{
m_entries.erase(it);
return true;
}
}
return false;
}
void MemoryWatchList::SetEntryDescription(u32 index, std::string description)
{
if (index >= m_entries.size())
return;
Entry& entry = m_entries[index];
entry.description = std::move(description);
}
void MemoryWatchList::SetEntryFreeze(u32 index, bool freeze)
{
if (index >= m_entries.size())
return;
Entry& entry = m_entries[index];
entry.freeze = freeze;
}
void MemoryWatchList::SetEntryValue(u32 index, u32 value)
{
if (index >= m_entries.size())
return;
Entry& entry = m_entries[index];
if (entry.value == value)
return;
SetEntryValue(&entry, value);
}
bool MemoryWatchList::RemoveEntryByDescription(const char* description)
{
bool result = false;
for (auto it = m_entries.begin(); it != m_entries.end();)
{
if (it->description == description)
{
it = m_entries.erase(it);
result = true;
continue;
}
++it;
}
return result;
}
void MemoryWatchList::UpdateValues()
{
for (Entry& entry : m_entries)
UpdateEntryValue(&entry);
}
void MemoryWatchList::SetEntryValue(Entry* entry, u32 value)
{
switch (entry->size)
{
case MemoryAccessSize::Byte:
CPU::SafeWriteMemoryByte(entry->address, Truncate8(value));
break;
case MemoryAccessSize::HalfWord:
CPU::SafeWriteMemoryHalfWord(entry->address, Truncate16(value));
break;
case MemoryAccessSize::Word:
CPU::SafeWriteMemoryWord(entry->address, value);
break;
}
entry->changed = (entry->value != value);
entry->value = value;
}
void MemoryWatchList::UpdateEntryValue(Entry* entry)
{
const u32 old_value = entry->value;
switch (entry->size)
{
case MemoryAccessSize::Byte:
{
u8 bvalue = 0;
if (CPU::SafeReadMemoryByte(entry->address, &bvalue)) [[likely]]
entry->value = entry->is_signed ? SignExtend32(bvalue) : ZeroExtend32(bvalue);
}
break;
case MemoryAccessSize::HalfWord:
{
u16 bvalue = 0;
if (CPU::SafeReadMemoryHalfWord(entry->address, &bvalue)) [[likely]]
entry->value = entry->is_signed ? SignExtend32(bvalue) : ZeroExtend32(bvalue);
}
break;
case MemoryAccessSize::Word:
{
CPU::SafeReadMemoryWord(entry->address, &entry->value);
}
break;
}
entry->changed = (old_value != entry->value);
if (entry->freeze && entry->changed)
SetEntryValue(entry, old_value);
}

130
src/core/memory_scanner.h Normal file
View File

@ -0,0 +1,130 @@
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com> and contributors.
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#pragma once
#include "types.h"
#include <optional>
#include <string>
#include <string_view>
#include <vector>
class MemoryScan
{
public:
enum class Operator
{
Any,
LessThanLast,
LessEqualLast,
GreaterThanLast,
GreaterEqualLast,
NotEqualLast,
EqualLast,
DecreasedBy,
IncreasedBy,
ChangedBy,
Equal,
NotEqual,
LessThan,
LessEqual,
GreaterThan,
GreaterEqual
};
struct Result
{
PhysicalMemoryAddress address;
u32 value;
u32 last_value;
bool value_changed;
bool Filter(Operator op, u32 comp_value, bool is_signed) const;
void UpdateValue(MemoryAccessSize size, bool is_signed);
};
using ResultVector = std::vector<Result>;
MemoryScan();
~MemoryScan();
u32 GetValue() const { return m_value; }
bool GetValueSigned() const { return m_signed; }
MemoryAccessSize GetSize() const { return m_size; }
Operator GetOperator() const { return m_operator; }
PhysicalMemoryAddress GetStartAddress() const { return m_start_address; }
PhysicalMemoryAddress GetEndAddress() const { return m_end_address; }
const ResultVector& GetResults() const { return m_results; }
const Result& GetResult(u32 index) const { return m_results[index]; }
u32 GetResultCount() const { return static_cast<u32>(m_results.size()); }
void SetValue(u32 value) { m_value = value; }
void SetValueSigned(bool s) { m_signed = s; }
void SetSize(MemoryAccessSize size) { m_size = size; }
void SetOperator(Operator op) { m_operator = op; }
void SetStartAddress(PhysicalMemoryAddress addr) { m_start_address = addr; }
void SetEndAddress(PhysicalMemoryAddress addr) { m_end_address = addr; }
void ResetSearch();
void Search();
void SearchAgain();
void UpdateResultsValues();
void SetResultValue(u32 index, u32 value);
private:
void SearchBytes();
void SearchHalfwords();
void SearchWords();
u32 m_value = 0;
MemoryAccessSize m_size = MemoryAccessSize::HalfWord;
Operator m_operator = Operator::Equal;
PhysicalMemoryAddress m_start_address = 0;
PhysicalMemoryAddress m_end_address = 0x200000;
ResultVector m_results;
bool m_signed = false;
};
class MemoryWatchList
{
public:
MemoryWatchList();
~MemoryWatchList();
struct Entry
{
std::string description;
u32 address;
u32 value;
MemoryAccessSize size;
bool is_signed;
bool freeze;
bool changed;
};
using EntryVector = std::vector<Entry>;
const Entry* GetEntryByAddress(u32 address) const;
const EntryVector& GetEntries() const { return m_entries; }
const Entry& GetEntry(u32 index) const { return m_entries[index]; }
u32 GetEntryCount() const { return static_cast<u32>(m_entries.size()); }
bool AddEntry(std::string description, u32 address, MemoryAccessSize size, bool is_signed, bool freeze);
void RemoveEntry(u32 index);
bool RemoveEntryByDescription(const char* description);
bool RemoveEntryByAddress(u32 address);
void SetEntryDescription(u32 index, std::string description);
void SetEntryFreeze(u32 index, bool freeze);
void SetEntryValue(u32 index, u32 value);
void UpdateValues();
private:
static void SetEntryValue(Entry* entry, u32 value);
static void UpdateEntryValue(Entry* entry);
EntryVector m_entries;
};

View File

@ -168,7 +168,6 @@ void Settings::Load(SettingsInterface& si, SettingsInterface& controller_si)
load_devices_from_save_states = si.GetBoolValue("Main", "LoadDevicesFromSaveStates", false);
apply_compatibility_settings = si.GetBoolValue("Main", "ApplyCompatibilitySettings", true);
apply_game_settings = si.GetBoolValue("Main", "ApplyGameSettings", true);
enable_cheats = si.GetBoolValue("Console", "EnableCheats", false);
disable_all_enhancements = si.GetBoolValue("Main", "DisableAllEnhancements", false);
enable_discord_presence = si.GetBoolValue("Main", "EnableDiscordPresence", false);
rewind_enable = si.GetBoolValue("Main", "RewindEnable", false);
@ -518,7 +517,6 @@ void Settings::Save(SettingsInterface& si, bool ignore_base) const
}
si.SetBoolValue("Main", "LoadDevicesFromSaveStates", load_devices_from_save_states);
si.SetBoolValue("Console", "EnableCheats", enable_cheats);
si.SetBoolValue("Main", "DisableAllEnhancements", disable_all_enhancements);
si.SetBoolValue("Main", "RewindEnable", rewind_enable);
si.SetFloatValue("Main", "RewindFrequency", rewind_save_frequency);
@ -938,7 +936,6 @@ void Settings::FixIncompatibleSettings(bool display_osd_messages)
g_settings.cpu_overclock_enable = false;
g_settings.cpu_overclock_active = false;
g_settings.enable_8mb_ram = false;
g_settings.enable_cheats = false;
g_settings.gpu_resolution_scale = 1;
g_settings.gpu_multisamples = 1;
g_settings.gpu_per_sample_shading = false;
@ -1051,7 +1048,6 @@ void Settings::FixIncompatibleSettings(bool display_osd_messages)
(g_settings.fast_forward_speed != 0.0f) ? std::max(g_settings.fast_forward_speed, 1.0f) : 0.0f;
g_settings.turbo_speed = (g_settings.turbo_speed != 0.0f) ? std::max(g_settings.turbo_speed, 1.0f) : 0.0f;
g_settings.rewind_enable = false;
g_settings.enable_cheats = false;
if (g_settings.cpu_overclock_enable && g_settings.GetCPUOverclockPercent() < 100)
{
g_settings.cpu_overclock_enable = false;
@ -2135,6 +2131,7 @@ std::string EmuFolders::GameIcons;
std::string EmuFolders::GameSettings;
std::string EmuFolders::InputProfiles;
std::string EmuFolders::MemoryCards;
std::string EmuFolders::Patches;
std::string EmuFolders::Resources;
std::string EmuFolders::SaveStates;
std::string EmuFolders::Screenshots;
@ -2154,6 +2151,7 @@ void EmuFolders::SetDefaults()
GameSettings = Path::Combine(DataRoot, "gamesettings");
InputProfiles = Path::Combine(DataRoot, "inputprofiles");
MemoryCards = Path::Combine(DataRoot, "memcards");
Patches = Path::Combine(DataRoot, "patches");
SaveStates = Path::Combine(DataRoot, "savestates");
Screenshots = Path::Combine(DataRoot, "screenshots");
Shaders = Path::Combine(DataRoot, "shaders");
@ -2185,6 +2183,7 @@ void EmuFolders::LoadConfig(SettingsInterface& si)
GameSettings = LoadPathFromSettings(si, DataRoot, "Folders", "GameSettings", "gamesettings");
InputProfiles = LoadPathFromSettings(si, DataRoot, "Folders", "InputProfiles", "inputprofiles");
MemoryCards = LoadPathFromSettings(si, DataRoot, "MemoryCards", "Directory", "memcards");
Patches = LoadPathFromSettings(si, DataRoot, "Folders", "Patches", "patches");
SaveStates = LoadPathFromSettings(si, DataRoot, "Folders", "SaveStates", "savestates");
Screenshots = LoadPathFromSettings(si, DataRoot, "Folders", "Screenshots", "screenshots");
Shaders = LoadPathFromSettings(si, DataRoot, "Folders", "Shaders", "shaders");
@ -2201,6 +2200,7 @@ void EmuFolders::LoadConfig(SettingsInterface& si)
DEV_LOG("Game Settings Directory: {}", GameSettings);
DEV_LOG("Input Profile Directory: {}", InputProfiles);
DEV_LOG("MemoryCards Directory: {}", MemoryCards);
DEV_LOG("Patches Directory: {}", Patches);
DEV_LOG("Resources Directory: {}", Resources);
DEV_LOG("SaveStates Directory: {}", SaveStates);
DEV_LOG("Screenshots Directory: {}", Screenshots);
@ -2222,6 +2222,7 @@ void EmuFolders::Save(SettingsInterface& si)
si.SetStringValue("Folders", "GameSettings", Path::MakeRelative(GameSettings, DataRoot).c_str());
si.SetStringValue("Folders", "InputProfiles", Path::MakeRelative(InputProfiles, DataRoot).c_str());
si.SetStringValue("MemoryCards", "Directory", Path::MakeRelative(MemoryCards, DataRoot).c_str());
si.SetStringValue("Folders", "Patches", Path::MakeRelative(Patches, DataRoot).c_str());
si.SetStringValue("Folders", "SaveStates", Path::MakeRelative(SaveStates, DataRoot).c_str());
si.SetStringValue("Folders", "Screenshots", Path::MakeRelative(Screenshots, DataRoot).c_str());
si.SetStringValue("Folders", "Shaders", Path::MakeRelative(Shaders, DataRoot).c_str());
@ -2262,6 +2263,7 @@ bool EmuFolders::EnsureFoldersExist()
result = FileSystem::EnsureDirectoryExists(GameSettings.c_str(), false) && result;
result = FileSystem::EnsureDirectoryExists(InputProfiles.c_str(), false) && result;
result = FileSystem::EnsureDirectoryExists(MemoryCards.c_str(), false) && result;
result = FileSystem::EnsureDirectoryExists(Patches.c_str(), false) && result;
result = FileSystem::EnsureDirectoryExists(SaveStates.c_str(), false) && result;
result = FileSystem::EnsureDirectoryExists(Screenshots.c_str(), false) && result;
result = FileSystem::EnsureDirectoryExists(Shaders.c_str(), false) && result;

View File

@ -88,7 +88,6 @@ struct Settings
bool load_devices_from_save_states : 1 = false;
bool apply_compatibility_settings : 1 = true;
bool apply_game_settings : 1 = true;
bool enable_cheats : 1 = false;
bool disable_all_enhancements : 1 = false;
bool enable_discord_presence : 1 = false;
@ -578,6 +577,7 @@ extern std::string GameIcons;
extern std::string GameSettings;
extern std::string InputProfiles;
extern std::string MemoryCards;
extern std::string Patches;
extern std::string Resources;
extern std::string SaveStates;
extern std::string Screenshots;

View File

@ -252,7 +252,7 @@ static std::string s_running_game_serial;
static std::string s_running_game_title;
static std::string s_exe_override;
static const GameDatabase::Entry* s_running_game_entry = nullptr;
static System::GameHash s_running_game_hash;
static GameHash s_running_game_hash;
static System::BootMode s_boot_mode = System::BootMode::None;
static bool s_running_game_custom_title = false;
@ -311,7 +311,6 @@ static Common::Timer s_fps_timer;
static Common::Timer s_frame_timer;
static Threading::ThreadHandle s_cpu_thread_handle;
static std::unique_ptr<CheatList> s_cheat_list;
static std::unique_ptr<MediaCapture> s_media_capture;
// temporary save state, created when loading, used to undo load state
@ -714,7 +713,7 @@ const GameDatabase::Entry* System::GetGameDatabaseEntry()
return s_running_game_entry;
}
System::GameHash System::GetGameHash()
GameHash System::GetGameHash()
{
return s_running_game_hash;
}
@ -922,7 +921,7 @@ bool System::GetGameDetailsFromImage(CDImage* cdi, std::string* out_id, GameHash
return true;
}
System::GameHash System::GetGameHashFromFile(const char* path)
GameHash System::GetGameHashFromFile(const char* path)
{
const std::optional<DynamicHeapArray<u8>> data = FileSystem::ReadBinaryFile(path);
if (!data)
@ -1061,8 +1060,8 @@ bool System::ReadExecutableFromImage(IsoReader& iso, std::string* out_executable
return true;
}
System::GameHash System::GetGameHashFromBuffer(std::string_view exe_name, std::span<const u8> exe_buffer,
const IsoReader::ISOPrimaryVolumeDescriptor& iso_pvd, u32 track_1_length)
GameHash System::GetGameHashFromBuffer(std::string_view exe_name, std::span<const u8> exe_buffer,
const IsoReader::ISOPrimaryVolumeDescriptor& iso_pvd, u32 track_1_length)
{
XXH64_state_t* state = XXH64_createState();
XXH64_reset(state, 0x4242D00C);
@ -1288,6 +1287,9 @@ void System::LoadSettings(bool display_osd_messages)
entry->ApplySettings(g_settings, display_osd_messages);
}
// patch overrides take precedence over compat settings
Cheats::ApplySettingOverrides();
g_settings.FixIncompatibleSettings(display_osd_messages);
}
@ -1483,6 +1485,9 @@ bool System::UpdateGameSettingsLayer()
Host::Internal::SetInputSettingsLayer(input_interface.get(), lock);
s_input_settings_interface = std::move(input_interface);
s_input_profile_name = std::move(input_profile_name);
Cheats::ReloadCheats(false, true, false, true);
return true;
}
@ -1496,9 +1501,8 @@ void System::ResetSystem()
if (Achievements::ResetHardcoreMode(false))
{
// Make sure a pre-existing cheat file hasn't been loaded when resetting
// after enabling HC mode.
s_cheat_list.reset();
// Make sure a pre-existing cheat file hasn't been loaded when resetting after enabling HC mode.
Cheats::ReloadCheats(true, true, false, true);
ApplySettings(false);
}
@ -1712,6 +1716,8 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error)
Error::SetStringFmt(error, "File '{}' is not a valid executable to boot.",
Path::GetFileName(parameters.override_exe));
s_state = State::Shutdown;
Cheats::UnloadAll();
ClearRunningGame();
Host::OnSystemDestroyed();
Host::OnIdleStateChanged();
return false;
@ -1726,6 +1732,7 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error)
if (!CheckForSBIFile(disc.get(), error))
{
s_state = State::Shutdown;
Cheats::UnloadAll();
ClearRunningGame();
Host::OnSystemDestroyed();
Host::OnIdleStateChanged();
@ -1763,6 +1770,7 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error)
if (cancelled)
{
s_state = State::Shutdown;
Cheats::UnloadAll();
ClearRunningGame();
Host::OnSystemDestroyed();
Host::OnIdleStateChanged();
@ -1776,6 +1784,7 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error)
if (!SetBootMode(boot_mode, disc_region, error))
{
s_state = State::Shutdown;
Cheats::UnloadAll();
ClearRunningGame();
Host::OnSystemDestroyed();
Host::OnIdleStateChanged();
@ -1787,6 +1796,7 @@ bool System::BootSystem(SystemBootParameters parameters, Error* error)
{
s_boot_mode = System::BootMode::None;
s_state = State::Shutdown;
Cheats::UnloadAll();
ClearRunningGame();
Host::OnSystemDestroyed();
Host::OnIdleStateChanged();
@ -1981,6 +1991,7 @@ void System::DestroySystem()
ClearMemorySaveStates();
Cheats::UnloadAll();
PCDrv::Shutdown();
SIO::Shutdown();
MDEC::Shutdown();
@ -2013,7 +2024,6 @@ void System::DestroySystem()
s_bios_image_info = nullptr;
s_exe_override = {};
s_boot_mode = BootMode::None;
s_cheat_list.reset();
s_state = State::Shutdown;
@ -2085,8 +2095,7 @@ void System::FrameDone()
// TODO: when running ahead, we can skip this (and the flush above)
SPU::GeneratePendingSamples();
if (s_cheat_list)
s_cheat_list->Apply();
Cheats::ApplyFrameEndCodes();
if (Achievements::IsActive())
Achievements::FrameUpdate();
@ -3598,33 +3607,6 @@ void System::DoFrameStep()
PauseSystem(false);
}
void System::DoToggleCheats()
{
if (!System::IsValid())
return;
if (Achievements::IsHardcoreModeActive())
{
Achievements::ConfirmHardcoreModeDisableAsync("Toggling cheats", [](bool approved) { DoToggleCheats(); });
return;
}
CheatList* cl = GetCheatList();
if (!cl)
{
Host::AddKeyedOSDMessage("ToggleCheats", TRANSLATE_STR("OSDMessage", "No cheats are loaded."), 10.0f);
return;
}
cl->SetMasterEnable(!cl->GetMasterEnable());
Host::AddIconOSDMessage(
"ToggleCheats", ICON_FA_EXCLAMATION_TRIANGLE,
cl->GetMasterEnable() ?
TRANSLATE_PLURAL_STR("System", "%n cheat(s) are now active.", "", cl->GetEnabledCodeCount()) :
TRANSLATE_PLURAL_STR("System", "%n cheat(s) are now inactive.", "", cl->GetEnabledCodeCount()),
Host::OSD_QUICK_DURATION);
}
#if 0
// currently not used until EXP1 is implemented
@ -4114,12 +4096,11 @@ void System::UpdateRunningGame(const std::string_view path, CDImage* image, bool
Achievements::GameChanged(s_running_game_path, image);
// game layer reloads cheats, but only the active list, we need new files
Cheats::ReloadCheats(true, false, false, true);
UpdateGameSettingsLayer();
ApplySettings(true);
s_cheat_list.reset();
if (g_settings.enable_cheats)
LoadCheatList();
ApplySettings(true);
if (s_running_game_serial != prev_serial)
UpdateSessionTime(prev_serial);
@ -4248,40 +4229,6 @@ bool System::SwitchMediaSubImage(u32 index)
return true;
}
bool System::HasCheatList()
{
return static_cast<bool>(s_cheat_list);
}
CheatList* System::GetCheatList()
{
return s_cheat_list.get();
}
void System::ApplyCheatCode(const CheatCode& code)
{
Assert(!IsShutdown());
code.Apply();
}
void System::SetCheatList(std::unique_ptr<CheatList> cheats)
{
Assert(!IsShutdown());
s_cheat_list = std::move(cheats);
if (s_cheat_list && s_cheat_list->GetEnabledCodeCount() > 0)
{
Host::AddIconOSDMessage("CheatsLoadWarning", ICON_FA_EXCLAMATION_TRIANGLE,
TRANSLATE_PLURAL_STR("System", "%n cheat(s) are enabled. This may crash games.", "",
s_cheat_list->GetEnabledCodeCount()),
Host::OSD_WARNING_DURATION);
}
else
{
Host::RemoveKeyedOSDMessage("CheatsLoadWarning");
}
}
void System::CheckForSettingsChanges(const Settings& old_settings)
{
if (IsValid() &&
@ -4390,14 +4337,6 @@ void System::CheckForSettingsChanges(const Settings& old_settings)
Bus::RemapFastmemViews();
}
if (g_settings.enable_cheats != old_settings.enable_cheats)
{
if (g_settings.enable_cheats)
LoadCheatList();
else
SetCheatList(nullptr);
}
SPU::GetOutputStream()->SetOutputVolume(GetAudioOutputVolume());
if (g_settings.gpu_resolution_scale != old_settings.gpu_resolution_scale ||
@ -4688,8 +4627,10 @@ void System::WarnAboutUnsafeSettings()
APPEND_SUBMESSAGE(TRANSLATE_SV("System", "Overclock disabled."));
if (g_settings.enable_8mb_ram)
APPEND_SUBMESSAGE(TRANSLATE_SV("System", "8MB RAM disabled."));
if (g_settings.enable_cheats)
if (s_game_settings_interface && s_game_settings_interface->GetBoolValue("Cheats", "EnableCheats", false))
APPEND_SUBMESSAGE(TRANSLATE_SV("System", "Cheats disabled."));
if (s_game_settings_interface && s_game_settings_interface->ContainsValue("Patches", "Enable"))
APPEND_SUBMESSAGE(TRANSLATE_SV("System", "Patches disabled."));
if (g_settings.gpu_resolution_scale != 1)
APPEND_SUBMESSAGE(TRANSLATE_SV("System", "Resolution scale set to 1x."));
if (g_settings.gpu_multisamples != 1)
@ -5585,154 +5526,6 @@ std::string System::GetCheatFileName()
return ret;
}
bool System::LoadCheatList()
{
// Called when booting, needs to test for shutdown.
if (IsShutdown() || !g_settings.enable_cheats)
return false;
const std::string filename(GetCheatFileName());
if (filename.empty() || !FileSystem::FileExists(filename.c_str()))
return false;
std::unique_ptr<CheatList> cl = std::make_unique<CheatList>();
if (!cl->LoadFromFile(filename.c_str(), CheatList::Format::Autodetect))
{
Host::AddIconOSDMessage(
"cheats_loaded", ICON_FA_EXCLAMATION_TRIANGLE,
fmt::format(TRANSLATE_FS("System", "Failed to load cheats from '{}'."), Path::GetFileName(filename)));
return false;
}
SetCheatList(std::move(cl));
return true;
}
bool System::LoadCheatListFromDatabase()
{
if (IsShutdown() || s_running_game_serial.empty() || Achievements::IsHardcoreModeActive())
return false;
std::unique_ptr<CheatList> cl = std::make_unique<CheatList>();
if (!cl->LoadFromPackage(s_running_game_serial))
return false;
INFO_LOG("Loaded {} cheats from database.", cl->GetCodeCount());
SetCheatList(std::move(cl));
return true;
}
bool System::SaveCheatList()
{
if (!System::IsValid() || !System::HasCheatList())
return false;
const std::string filename(GetCheatFileName());
if (filename.empty())
return false;
if (!System::GetCheatList()->SaveToPCSXRFile(filename.c_str()))
{
Host::AddIconOSDMessage(
"CheatSaveError", ICON_FA_EXCLAMATION_TRIANGLE,
fmt::format(TRANSLATE_FS("System", "Failed to save cheat list to '{}'."), Path::GetFileName(filename)),
Host::OSD_ERROR_DURATION);
}
return true;
}
bool System::DeleteCheatList()
{
if (!System::IsValid())
return false;
const std::string filename(GetCheatFileName());
if (!filename.empty())
{
if (!FileSystem::DeleteFile(filename.c_str()))
return false;
Host::AddIconOSDMessage(
"CheatDelete", ICON_FA_EXCLAMATION_TRIANGLE,
fmt::format(TRANSLATE_FS("System", "Deleted cheat list '{}'."), Path::GetFileName(filename)),
Host::OSD_INFO_DURATION);
}
System::SetCheatList(nullptr);
return true;
}
void System::ClearCheatList(bool save_to_file)
{
if (!System::IsValid())
return;
CheatList* cl = System::GetCheatList();
if (!cl)
return;
while (cl->GetCodeCount() > 0)
cl->RemoveCode(cl->GetCodeCount() - 1);
if (save_to_file)
SaveCheatList();
}
void System::SetCheatCodeState(u32 index, bool enabled)
{
if (!System::IsValid() || !System::HasCheatList())
return;
CheatList* cl = System::GetCheatList();
if (index >= cl->GetCodeCount())
return;
CheatCode& cc = cl->GetCode(index);
if (cc.enabled == enabled)
return;
cc.enabled = enabled;
if (!enabled)
cc.ApplyOnDisable();
if (enabled)
{
Host::AddIconOSDMessage(fmt::format("Cheat{}State", index), ICON_FA_EXCLAMATION_TRIANGLE,
fmt::format(TRANSLATE_FS("System", "Cheat '{}' enabled."), cc.description),
Host::OSD_INFO_DURATION);
}
else
{
Host::AddIconOSDMessage(fmt::format("Cheat{}State", index), ICON_FA_EXCLAMATION_TRIANGLE,
fmt::format(TRANSLATE_FS("System", "Cheat '{}' disabled."), cc.description),
Host::OSD_INFO_DURATION);
}
SaveCheatList();
}
void System::ApplyCheatCode(u32 index)
{
if (!System::HasCheatList() || index >= System::GetCheatList()->GetCodeCount())
return;
const CheatCode& cc = System::GetCheatList()->GetCode(index);
if (!cc.enabled)
{
cc.Apply();
Host::AddIconOSDMessage(fmt::format("Cheat{}State", index), ICON_FA_EXCLAMATION_TRIANGLE,
fmt::format(TRANSLATE_FS("System", "Applied cheat '{}'."), cc.description),
Host::OSD_INFO_DURATION);
}
else
{
Host::AddIconOSDMessage(fmt::format("Cheat{}State", index), ICON_FA_EXCLAMATION_TRIANGLE,
fmt::format(TRANSLATE_FS("System", "Cheat '{}' is already enabled."), cc.description),
Host::OSD_INFO_DURATION);
}
}
void System::ToggleWidescreen()
{
g_settings.gpu_widescreen_hack = !g_settings.gpu_widescreen_hack;

View File

@ -23,9 +23,6 @@ enum class GPUVSyncMode : u8;
class Controller;
struct CheatCode;
class CheatList;
class GPUTexture;
class MediaCapture;
@ -107,8 +104,6 @@ enum class BootMode
BootPSF,
};
using GameHash = u64;
extern TickCount g_ticks_per_second;
/// Returns true if the filename is a PlayStation executable we can inject.
@ -315,18 +310,6 @@ std::string GetMediaSubImageTitle(u32 index);
/// Switches to the specified media/disc playlist index.
bool SwitchMediaSubImage(u32 index);
/// Returns true if there is currently a cheat list.
bool HasCheatList();
/// Accesses the current cheat list.
CheatList* GetCheatList();
/// Applies a single cheat code.
void ApplyCheatCode(const CheatCode& code);
/// Sets or clears the provided cheat list, applying every frame.
void SetCheatList(std::unique_ptr<CheatList> cheats);
/// Updates throttler.
void UpdateSpeedLimiterState();
@ -343,7 +326,6 @@ bool IsRewinding();
void SetRewindState(bool enabled);
void DoFrameStep();
void DoToggleCheats();
/// Returns the path to a save state file. Specifying an index of -1 is the "resume" save state.
std::string GetGameSaveStateFileName(std::string_view serial, s32 slot);
@ -405,27 +387,6 @@ bool StartMediaCapture(std::string path = {});
bool StartMediaCapture(std::string path, bool capture_video, bool capture_audio);
void StopMediaCapture();
/// Loads the cheat list for the current game title from the user directory.
bool LoadCheatList();
/// Loads the cheat list for the current game code from the built-in code database.
bool LoadCheatListFromDatabase();
/// Saves the current cheat list to the game title's file.
bool SaveCheatList();
/// Deletes the cheat list, if present.
bool DeleteCheatList();
/// Removes all cheats from the cheat list.
void ClearCheatList(bool save_to_file);
/// Enables/disabled the specified cheat code.
void SetCheatCodeState(u32 index, bool enabled);
/// Immediately applies the specified cheat code.
void ApplyCheatCode(u32 index);
/// Toggle Widescreen Hack and Aspect Ratio
void ToggleWidescreen();

View File

@ -22,6 +22,7 @@ enum class MemoryAccessSize : u32
using TickCount = s32;
using GlobalTicks = u64;
using GameHash = u64;
enum class ConsoleRegion : u8
{

View File

@ -29,12 +29,6 @@ set(SRCS
biossettingswidget.cpp
biossettingswidget.h
biossettingswidget.ui
cheatcodeeditordialog.cpp
cheatcodeeditordialog.h
cheatcodeeditordialog.ui
cheatmanagerwindow.cpp
cheatmanagerwindow.h
cheatmanagerwindow.ui
colorpickerbutton.cpp
colorpickerbutton.h
consolesettingswidget.cpp
@ -79,6 +73,15 @@ set(SRCS
foldersettingswidget.cpp
foldersettingswidget.h
foldersettingswidget.ui
gamecheatcodechoiceeditordialog.ui
gamecheatcodeeditordialog.ui
gamecheatsettingswidget.cpp
gamecheatsettingswidget.h
gamecheatsettingswidget.ui
gamepatchdetailswidget.ui
gamepatchsettingswidget.cpp
gamepatchsettingswidget.h
gamepatchsettingswidget.ui
gamelistmodel.cpp
gamelistmodel.h
gamelistrefreshthread.cpp

View File

@ -1,92 +0,0 @@
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#include "cheatcodeeditordialog.h"
#include <QtWidgets/QMessageBox>
CheatCodeEditorDialog::CheatCodeEditorDialog(const QStringList& group_names, CheatCode* code, QWidget* parent)
: QDialog(parent), m_code(code)
{
m_ui.setupUi(this);
setupAdditionalUi(group_names);
fillUi();
connectUi();
}
CheatCodeEditorDialog::~CheatCodeEditorDialog() = default;
void CheatCodeEditorDialog::saveClicked()
{
std::string new_description = m_ui.description->text().toStdString();
if (new_description.empty())
{
QMessageBox::critical(this, tr("Error"), tr("Description cannot be empty."));
return;
}
if (!m_code->SetInstructionsFromString(m_ui.instructions->toPlainText().toStdString()))
{
QMessageBox::critical(this, tr("Error"), tr("Instructions are invalid."));
return;
}
m_code->description = std::move(new_description);
m_code->type = static_cast<CheatCode::Type>(m_ui.type->currentIndex());
m_code->activation = static_cast<CheatCode::Activation>(m_ui.activation->currentIndex());
m_code->group = m_ui.group->currentText().toStdString();
done(1);
}
void CheatCodeEditorDialog::cancelClicked()
{
done(0);
}
void CheatCodeEditorDialog::setupAdditionalUi(const QStringList& group_names)
{
for (u32 i = 0; i < static_cast<u32>(CheatCode::Type::Count); i++)
{
m_ui.type->addItem(qApp->translate("Cheats", CheatCode::GetTypeDisplayName(static_cast<CheatCode::Type>(i))));
}
for (u32 i = 0; i < static_cast<u32>(CheatCode::Activation::Count); i++)
{
m_ui.activation->addItem(
qApp->translate("Cheats", CheatCode::GetActivationDisplayName(static_cast<CheatCode::Activation>(i))));
}
if (!group_names.isEmpty())
m_ui.group->addItems(group_names);
else
m_ui.group->addItem(QStringLiteral("Ungrouped"));
}
void CheatCodeEditorDialog::fillUi()
{
m_ui.description->setText(QString::fromStdString(m_code->description));
const QString group_qstr(QString::fromStdString(m_code->group));
int index = m_ui.group->findText(group_qstr);
if (index >= 0)
{
m_ui.group->setCurrentIndex(index);
}
else
{
index = m_ui.group->count();
m_ui.group->addItem(group_qstr);
m_ui.group->setCurrentIndex(index);
}
m_ui.type->setCurrentIndex(static_cast<int>(m_code->type));
m_ui.activation->setCurrentIndex(static_cast<int>(m_code->activation));
m_ui.instructions->setPlainText(QString::fromStdString(m_code->GetInstructionsAsString()));
}
void CheatCodeEditorDialog::connectUi()
{
connect(m_ui.buttonBox, &QDialogButtonBox::accepted, this, &CheatCodeEditorDialog::saveClicked);
connect(m_ui.buttonBox, &QDialogButtonBox::rejected, this, &CheatCodeEditorDialog::cancelClicked);
}

View File

@ -1,28 +0,0 @@
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#pragma once
#include "core/cheats.h"
#include "ui_cheatcodeeditordialog.h"
class CheatCodeEditorDialog : public QDialog
{
Q_OBJECT
public:
CheatCodeEditorDialog(const QStringList& group_names, CheatCode* code, QWidget* parent);
~CheatCodeEditorDialog();
private Q_SLOTS:
void saveClicked();
void cancelClicked();
private:
void setupAdditionalUi(const QStringList& group_names);
void fillUi();
void connectUi();
CheatCode* m_code;
Ui::CheatCodeEditorDialog m_ui;
};

View File

@ -1,74 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CheatCodeEditorDialog</class>
<widget class="QDialog" name="CheatCodeEditorDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>491</width>
<height>284</height>
</rect>
</property>
<property name="windowTitle">
<string>Cheat Code Editor</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Description:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="description"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Group:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="group"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Type:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="type"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Activation:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="activation"/>
</item>
<item row="4" column="0" colspan="2">
<widget class="QPlainTextEdit" name="instructions"/>
</item>
<item row="5" column="0" colspan="2">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Save</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -1,581 +0,0 @@
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com> and contributors.
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#include "cheatmanagerwindow.h"
#include "cheatcodeeditordialog.h"
#include "mainwindow.h"
#include "qthost.h"
#include "qtutils.h"
#include "core/bus.h"
#include "core/cpu_core.h"
#include "core/host.h"
#include "core/system.h"
#include "common/assert.h"
#include "common/string_util.h"
#include <QtCore/QFileInfo>
#include <QtGui/QColor>
#include <QtWidgets/QFileDialog>
#include <QtWidgets/QInputDialog>
#include <QtWidgets/QMenu>
#include <QtWidgets/QMessageBox>
#include <QtWidgets/QTreeWidgetItemIterator>
#include <array>
#include <utility>
CheatManagerWindow::CheatManagerWindow() : QWidget()
{
m_ui.setupUi(this);
QtUtils::RestoreWindowGeometry("CheatManagerWindow", this);
connectUi();
updateCheatList();
}
CheatManagerWindow::~CheatManagerWindow() = default;
void CheatManagerWindow::connectUi()
{
connect(m_ui.cheatList, &QTreeWidget::currentItemChanged, this, &CheatManagerWindow::cheatListCurrentItemChanged);
connect(m_ui.cheatList, &QTreeWidget::itemActivated, this, &CheatManagerWindow::cheatListItemActivated);
connect(m_ui.cheatList, &QTreeWidget::itemChanged, this, &CheatManagerWindow::cheatListItemChanged);
connect(m_ui.cheatListNewCategory, &QPushButton::clicked, this, &CheatManagerWindow::newCategoryClicked);
connect(m_ui.cheatListAdd, &QPushButton::clicked, this, &CheatManagerWindow::addCodeClicked);
connect(m_ui.cheatListEdit, &QPushButton::clicked, this, &CheatManagerWindow::editCodeClicked);
connect(m_ui.cheatListRemove, &QPushButton::clicked, this, &CheatManagerWindow::deleteCodeClicked);
connect(m_ui.cheatListActivate, &QPushButton::clicked, this, &CheatManagerWindow::activateCodeClicked);
connect(m_ui.cheatListImport, &QPushButton::clicked, this, &CheatManagerWindow::importClicked);
connect(m_ui.cheatListExport, &QPushButton::clicked, this, &CheatManagerWindow::exportClicked);
connect(m_ui.cheatListClear, &QPushButton::clicked, this, &CheatManagerWindow::clearClicked);
connect(m_ui.cheatListReset, &QPushButton::clicked, this, &CheatManagerWindow::resetClicked);
connect(g_emu_thread, &EmuThread::cheatEnabled, this, &CheatManagerWindow::setCheatCheckState);
connect(g_emu_thread, &EmuThread::runningGameChanged, this, &CheatManagerWindow::updateCheatList);
}
void CheatManagerWindow::showEvent(QShowEvent* event)
{
QWidget::showEvent(event);
resizeColumns();
}
void CheatManagerWindow::closeEvent(QCloseEvent* event)
{
QtUtils::SaveWindowGeometry("CheatManagerWindow", this);
QWidget::closeEvent(event);
emit closed();
}
void CheatManagerWindow::resizeEvent(QResizeEvent* event)
{
QWidget::resizeEvent(event);
resizeColumns();
}
void CheatManagerWindow::resizeColumns()
{
QtUtils::ResizeColumnsForTreeView(m_ui.cheatList, {-1, 100, 150, 100});
}
QTreeWidgetItem* CheatManagerWindow::getItemForCheatIndex(u32 index) const
{
QTreeWidgetItemIterator iter(m_ui.cheatList);
while (*iter)
{
QTreeWidgetItem* item = *iter;
const QVariant item_data(item->data(0, Qt::UserRole));
if (item_data.isValid() && item_data.toUInt() == index)
return item;
++iter;
}
return nullptr;
}
QTreeWidgetItem* CheatManagerWindow::getItemForCheatGroup(const QString& group_name) const
{
const int count = m_ui.cheatList->topLevelItemCount();
for (int i = 0; i < count; i++)
{
QTreeWidgetItem* item = m_ui.cheatList->topLevelItem(i);
if (item->text(0) == group_name)
return item;
}
return nullptr;
}
QTreeWidgetItem* CheatManagerWindow::createItemForCheatGroup(const QString& group_name) const
{
QTreeWidgetItem* group = new QTreeWidgetItem();
group->setFlags(group->flags() | Qt::ItemIsUserCheckable);
group->setText(0, group_name);
m_ui.cheatList->addTopLevelItem(group);
return group;
}
QStringList CheatManagerWindow::getCheatGroupNames() const
{
QStringList group_names;
const int count = m_ui.cheatList->topLevelItemCount();
for (int i = 0; i < count; i++)
{
QTreeWidgetItem* item = m_ui.cheatList->topLevelItem(i);
group_names.push_back(item->text(0));
}
return group_names;
}
static int getCheatIndexFromItem(QTreeWidgetItem* item)
{
QVariant item_data(item->data(0, Qt::UserRole));
if (!item_data.isValid())
return -1;
return static_cast<int>(item_data.toUInt());
}
int CheatManagerWindow::getSelectedCheatIndex() const
{
QList<QTreeWidgetItem*> sel = m_ui.cheatList->selectedItems();
if (sel.isEmpty())
return -1;
return static_cast<int>(getCheatIndexFromItem(sel.first()));
}
CheatList* CheatManagerWindow::getCheatList() const
{
return System::IsValid() ? System::GetCheatList() : nullptr;
}
void CheatManagerWindow::updateCheatList()
{
QSignalBlocker sb(m_ui.cheatList);
while (m_ui.cheatList->topLevelItemCount() > 0)
delete m_ui.cheatList->takeTopLevelItem(0);
m_ui.cheatList->setEnabled(false);
m_ui.cheatListAdd->setEnabled(false);
m_ui.cheatListNewCategory->setEnabled(false);
m_ui.cheatListEdit->setEnabled(false);
m_ui.cheatListRemove->setEnabled(false);
m_ui.cheatListActivate->setText(tr("Activate"));
m_ui.cheatListActivate->setEnabled(false);
m_ui.cheatListImport->setEnabled(false);
m_ui.cheatListExport->setEnabled(false);
m_ui.cheatListClear->setEnabled(false);
m_ui.cheatListReset->setEnabled(false);
Host::RunOnCPUThread([]() {
if (!System::IsValid())
return;
CheatList* list = System::GetCheatList();
if (!list)
{
System::LoadCheatList();
list = System::GetCheatList();
}
if (!list)
{
System::LoadCheatListFromDatabase();
list = System::GetCheatList();
}
if (!list)
{
System::SetCheatList(std::make_unique<CheatList>());
list = System::GetCheatList();
}
// still racey...
QtHost::RunOnUIThread([list]() {
if (!QtHost::IsSystemValid())
return;
CheatManagerWindow* cm = g_main_window->getCheatManagerWindow();
if (!cm)
return;
QSignalBlocker sb(cm->m_ui.cheatList);
const std::vector<std::string> groups = list->GetCodeGroups();
for (const std::string& group_name : groups)
{
QTreeWidgetItem* group = cm->createItemForCheatGroup(QString::fromStdString(group_name));
const u32 count = list->GetCodeCount();
bool all_enabled = true;
for (u32 i = 0; i < count; i++)
{
const CheatCode& code = list->GetCode(i);
if (code.group != group_name)
continue;
QTreeWidgetItem* item = new QTreeWidgetItem(group);
cm->fillItemForCheatCode(item, i, code);
all_enabled &= code.enabled;
}
group->setCheckState(0, all_enabled ? Qt::Checked : Qt::Unchecked);
group->setExpanded(true);
}
cm->m_ui.cheatList->setEnabled(true);
cm->m_ui.cheatListAdd->setEnabled(true);
cm->m_ui.cheatListNewCategory->setEnabled(true);
cm->m_ui.cheatListImport->setEnabled(true);
cm->m_ui.cheatListClear->setEnabled(true);
cm->m_ui.cheatListReset->setEnabled(true);
cm->m_ui.cheatListExport->setEnabled(cm->m_ui.cheatList->topLevelItemCount() > 0);
});
});
}
void CheatManagerWindow::fillItemForCheatCode(QTreeWidgetItem* item, u32 index, const CheatCode& code)
{
item->setData(0, Qt::UserRole, QVariant(static_cast<uint>(index)));
if (code.IsManuallyActivated())
{
item->setFlags(item->flags() & ~(Qt::ItemIsUserCheckable));
}
else
{
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
item->setCheckState(0, code.enabled ? Qt::Checked : Qt::Unchecked);
}
item->setText(0, QString::fromStdString(code.description));
item->setText(1, qApp->translate("Cheats", CheatCode::GetTypeDisplayName(code.type)));
item->setText(2, qApp->translate("Cheats", CheatCode::GetActivationDisplayName(code.activation)));
item->setText(3, QString::number(static_cast<uint>(code.instructions.size())));
}
void CheatManagerWindow::saveCheatList()
{
Host::RunOnCPUThread([]() { System::SaveCheatList(); });
}
void CheatManagerWindow::cheatListCurrentItemChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous)
{
const int cheat_index = current ? getCheatIndexFromItem(current) : -1;
const bool has_current = (cheat_index >= 0);
m_ui.cheatListEdit->setEnabled(has_current);
m_ui.cheatListRemove->setEnabled(has_current);
m_ui.cheatListActivate->setEnabled(has_current);
if (!has_current)
{
m_ui.cheatListActivate->setText(tr("Activate"));
}
else
{
const bool manual_activation = getCheatList()->GetCode(static_cast<u32>(cheat_index)).IsManuallyActivated();
m_ui.cheatListActivate->setText(manual_activation ? tr("Activate") : tr("Toggle"));
}
}
void CheatManagerWindow::cheatListItemActivated(QTreeWidgetItem* item)
{
if (!item)
return;
const int index = getCheatIndexFromItem(item);
if (index >= 0)
activateCheat(static_cast<u32>(index));
}
void CheatManagerWindow::cheatListItemChanged(QTreeWidgetItem* item, int column)
{
if (!item || column != 0)
return;
CheatList* list = getCheatList();
const int index = getCheatIndexFromItem(item);
if (index < 0)
{
// we're probably a parent/group node
const int child_count = item->childCount();
const Qt::CheckState cs = item->checkState(0);
for (int i = 0; i < child_count; i++)
item->child(i)->setCheckState(0, cs);
return;
}
if (static_cast<u32>(index) >= list->GetCodeCount())
return;
CheatCode& cc = list->GetCode(static_cast<u32>(index));
if (cc.IsManuallyActivated())
return;
const bool new_enabled = (item->checkState(0) == Qt::Checked);
if (cc.enabled == new_enabled)
return;
Host::RunOnCPUThread([index, new_enabled]() {
System::GetCheatList()->SetCodeEnabled(static_cast<u32>(index), new_enabled);
System::SaveCheatList();
});
}
void CheatManagerWindow::activateCheat(u32 index)
{
CheatList* list = getCheatList();
if (index >= list->GetCodeCount())
return;
CheatCode& cc = list->GetCode(index);
if (cc.IsManuallyActivated())
{
g_emu_thread->applyCheat(index);
return;
}
const bool new_enabled = !cc.enabled;
setCheatCheckState(index, new_enabled);
Host::RunOnCPUThread([index, new_enabled]() {
System::GetCheatList()->SetCodeEnabled(index, new_enabled);
System::SaveCheatList();
});
}
void CheatManagerWindow::setCheatCheckState(u32 index, bool checked)
{
QTreeWidgetItem* item = getItemForCheatIndex(index);
if (item)
{
QSignalBlocker sb(m_ui.cheatList);
item->setCheckState(0, checked ? Qt::Checked : Qt::Unchecked);
}
}
void CheatManagerWindow::newCategoryClicked()
{
QString group_name = QInputDialog::getText(this, tr("Add Group"), tr("Group Name:"));
if (group_name.isEmpty())
return;
if (getItemForCheatGroup(group_name) != nullptr)
{
QMessageBox::critical(this, tr("Error"), tr("This group name already exists."));
return;
}
createItemForCheatGroup(group_name);
}
void CheatManagerWindow::addCodeClicked()
{
CheatList* list = getCheatList();
CheatCode new_code;
new_code.group = "Ungrouped";
CheatCodeEditorDialog editor(getCheatGroupNames(), &new_code, this);
if (editor.exec() > 0)
{
const QString group_name_qstr(QString::fromStdString(new_code.group));
QTreeWidgetItem* group_item = getItemForCheatGroup(group_name_qstr);
if (!group_item)
group_item = createItemForCheatGroup(group_name_qstr);
QTreeWidgetItem* item = new QTreeWidgetItem(group_item);
fillItemForCheatCode(item, list->GetCodeCount(), new_code);
group_item->setExpanded(true);
Host::RunOnCPUThread(
[&new_code]() {
System::GetCheatList()->AddCode(std::move(new_code));
System::SaveCheatList();
},
true);
}
}
void CheatManagerWindow::editCodeClicked()
{
int index = getSelectedCheatIndex();
if (index < 0)
return;
CheatList* list = getCheatList();
if (static_cast<u32>(index) >= list->GetCodeCount())
return;
CheatCode new_code = list->GetCode(static_cast<u32>(index));
CheatCodeEditorDialog editor(getCheatGroupNames(), &new_code, this);
if (editor.exec() > 0)
{
QTreeWidgetItem* item = getItemForCheatIndex(static_cast<u32>(index));
if (item)
{
if (new_code.group != list->GetCode(static_cast<u32>(index)).group)
{
item = item->parent()->takeChild(item->parent()->indexOfChild(item));
const QString group_name_qstr(QString::fromStdString(new_code.group));
QTreeWidgetItem* group_item = getItemForCheatGroup(group_name_qstr);
if (!group_item)
group_item = createItemForCheatGroup(group_name_qstr);
group_item->addChild(item);
group_item->setExpanded(true);
}
fillItemForCheatCode(item, static_cast<u32>(index), new_code);
}
else
{
// shouldn't happen...
updateCheatList();
}
Host::RunOnCPUThread(
[index, &new_code]() {
System::GetCheatList()->SetCode(static_cast<u32>(index), std::move(new_code));
System::SaveCheatList();
},
true);
}
}
void CheatManagerWindow::deleteCodeClicked()
{
int index = getSelectedCheatIndex();
if (index < 0)
return;
CheatList* list = getCheatList();
if (static_cast<u32>(index) >= list->GetCodeCount())
return;
if (QMessageBox::question(this, tr("Delete Code"),
tr("Are you sure you wish to delete the selected code? This action is not reversible."),
QMessageBox::Yes, QMessageBox::No) != QMessageBox::Yes)
{
return;
}
Host::RunOnCPUThread(
[index]() {
System::GetCheatList()->RemoveCode(static_cast<u32>(index));
System::SaveCheatList();
},
true);
updateCheatList();
}
void CheatManagerWindow::activateCodeClicked()
{
int index = getSelectedCheatIndex();
if (index < 0)
return;
activateCheat(static_cast<u32>(index));
}
void CheatManagerWindow::importClicked()
{
QMenu menu(this);
connect(menu.addAction(tr("From File...")), &QAction::triggered, this, &CheatManagerWindow::importFromFileTriggered);
connect(menu.addAction(tr("From Text...")), &QAction::triggered, this, &CheatManagerWindow::importFromTextTriggered);
menu.exec(QCursor::pos());
}
void CheatManagerWindow::importFromFileTriggered()
{
const QString filter(tr("PCSXR/Libretro Cheat Files (*.cht *.txt);;All Files (*.*)"));
const QString filename =
QDir::toNativeSeparators(QFileDialog::getOpenFileName(this, tr("Import Cheats"), QString(), filter));
if (filename.isEmpty())
return;
CheatList new_cheats;
if (!new_cheats.LoadFromFile(filename.toUtf8().constData(), CheatList::Format::Autodetect))
{
QMessageBox::critical(this, tr("Error"), tr("Failed to parse cheat file. The log may contain more information."));
return;
}
Host::RunOnCPUThread(
[&new_cheats]() {
DebugAssert(System::HasCheatList());
System::GetCheatList()->MergeList(new_cheats);
System::SaveCheatList();
},
true);
updateCheatList();
}
void CheatManagerWindow::importFromTextTriggered()
{
const QString text = QInputDialog::getMultiLineText(this, tr("Import Cheats"), tr("Cheat File Text:"));
if (text.isEmpty())
return;
CheatList new_cheats;
if (!new_cheats.LoadFromString(text.toStdString(), CheatList::Format::Autodetect))
{
QMessageBox::critical(this, tr("Error"), tr("Failed to parse cheat file. The log may contain more information."));
return;
}
Host::RunOnCPUThread(
[&new_cheats]() {
DebugAssert(System::HasCheatList());
System::GetCheatList()->MergeList(new_cheats);
System::SaveCheatList();
},
true);
updateCheatList();
}
void CheatManagerWindow::exportClicked()
{
const QString filter(tr("PCSXR Cheat Files (*.cht);;All Files (*.*)"));
const QString filename =
QDir::toNativeSeparators(QFileDialog::getSaveFileName(this, tr("Export Cheats"), QString(), filter));
if (filename.isEmpty())
return;
if (!getCheatList()->SaveToPCSXRFile(filename.toUtf8().constData()))
QMessageBox::critical(this, tr("Error"), tr("Failed to save cheat file. The log may contain more information."));
}
void CheatManagerWindow::clearClicked()
{
if (QMessageBox::question(this, tr("Confirm Clear"),
tr("Are you sure you want to remove all cheats? This is not reversible.")) !=
QMessageBox::Yes)
{
return;
}
Host::RunOnCPUThread([] { System::ClearCheatList(true); }, true);
updateCheatList();
}
void CheatManagerWindow::resetClicked()
{
if (QMessageBox::question(
this, tr("Confirm Reset"),
tr(
"Are you sure you want to reset the cheat list? Any cheats not in the DuckStation database WILL BE LOST.")) !=
QMessageBox::Yes)
{
return;
}
Host::RunOnCPUThread([] { System::DeleteCheatList(); }, true);
updateCheatList();
}

View File

@ -1,74 +0,0 @@
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#pragma once
#include "ui_cheatmanagerwindow.h"
#include "core/cheats.h"
#include <QtCore/QTimer>
#include <QtWidgets/QComboBox>
#include <QtWidgets/QLabel>
#include <QtWidgets/QPushButton>
#include <QtWidgets/QTableWidget>
#include <QtWidgets/QWidget>
#include <optional>
class CheatManagerWindow : public QWidget
{
Q_OBJECT
public:
CheatManagerWindow();
~CheatManagerWindow();
Q_SIGNALS:
void closed();
protected:
void showEvent(QShowEvent* event);
void closeEvent(QCloseEvent* event);
void resizeEvent(QResizeEvent* event);
void resizeColumns();
private Q_SLOTS:
CheatList* getCheatList() const;
void updateCheatList();
void saveCheatList();
void cheatListCurrentItemChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous);
void cheatListItemActivated(QTreeWidgetItem* item);
void cheatListItemChanged(QTreeWidgetItem* item, int column);
void activateCheat(u32 index);
void setCheatCheckState(u32 index, bool checked);
void newCategoryClicked();
void addCodeClicked();
void editCodeClicked();
void deleteCodeClicked();
void activateCodeClicked();
void importClicked();
void importFromFileTriggered();
void importFromTextTriggered();
void exportClicked();
void clearClicked();
void resetClicked();
private:
enum : int
{
MAX_DISPLAYED_SCAN_RESULTS = 5000
};
void connectUi();
void fillItemForCheatCode(QTreeWidgetItem* item, u32 index, const CheatCode& code);
QTreeWidgetItem* getItemForCheatIndex(u32 index) const;
QTreeWidgetItem* getItemForCheatGroup(const QString& group_name) const;
QTreeWidgetItem* createItemForCheatGroup(const QString& group_name) const;
QStringList getCheatGroupNames() const;
int getSelectedCheatIndex() const;
Ui::CheatManagerWindow m_ui;
QTimer* m_update_timer = nullptr;
};

View File

@ -1,144 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CheatManagerWindow</class>
<widget class="QWidget" name="CheatManagerWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>817</width>
<height>462</height>
</rect>
</property>
<property name="windowTitle">
<string>Cheat Manager</string>
</property>
<property name="windowIcon">
<iconset>
<normaloff>:/icons/duck.png</normaloff>:/icons/duck.png</iconset>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="0">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="cheatListNewCategory">
<property name="text">
<string>&amp;Add Group...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cheatListAdd">
<property name="text">
<string>&amp;Add Code...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cheatListEdit">
<property name="text">
<string>&amp;Edit Code...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cheatListRemove">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>&amp;Delete Code</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cheatListActivate">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Activate</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cheatListImport">
<property name="text">
<string>Import...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cheatListExport">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Export...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cheatListClear">
<property name="text">
<string>Clear</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cheatListReset">
<property name="text">
<string>Reset</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="0" column="0">
<widget class="QTreeWidget" name="cheatList">
<property name="selectionMode">
<enum>QAbstractItemView::SelectionMode::SingleSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectionBehavior::SelectRows</enum>
</property>
<column>
<property name="text">
<string>Name</string>
</property>
</column>
<column>
<property name="text">
<string>Type</string>
</property>
</column>
<column>
<property name="text">
<string>Activation</string>
</property>
</column>
<column>
<property name="text">
<string>Instructions</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -9,8 +9,6 @@
<ClCompile Include="audiosettingswidget.cpp" />
<ClCompile Include="autoupdaterdialog.cpp" />
<ClCompile Include="biossettingswidget.cpp" />
<ClCompile Include="cheatmanagerwindow.cpp" />
<ClCompile Include="cheatcodeeditordialog.cpp" />
<ClCompile Include="colorpickerbutton.cpp" />
<ClCompile Include="consolesettingswidget.cpp" />
<ClCompile Include="controllerbindingwidgets.cpp" />
@ -21,7 +19,9 @@
<ClCompile Include="debuggermodels.cpp" />
<ClCompile Include="debuggerwindow.cpp" />
<ClCompile Include="foldersettingswidget.cpp" />
<ClCompile Include="gamecheatsettingswidget.cpp" />
<ClCompile Include="gamelistmodel.cpp" />
<ClCompile Include="gamepatchsettingswidget.cpp" />
<ClCompile Include="interfacesettingswidget.cpp" />
<ClCompile Include="graphicssettingswidget.cpp" />
<ClCompile Include="hotkeysettingswidget.cpp" />
@ -58,8 +58,6 @@
<QtMoc Include="aboutdialog.h" />
<QtMoc Include="audiosettingswidget.h" />
<QtMoc Include="biossettingswidget.h" />
<QtMoc Include="cheatmanagerwindow.h" />
<QtMoc Include="cheatcodeeditordialog.h" />
<QtMoc Include="coverdownloaddialog.h" />
<QtMoc Include="memorycardsettingswidget.h" />
<QtMoc Include="memorycardeditorwindow.h" />
@ -85,6 +83,8 @@
<QtMoc Include="logwindow.h" />
<QtMoc Include="graphicssettingswidget.h" />
<QtMoc Include="memoryscannerwindow.h" />
<QtMoc Include="gamecheatsettingswidget.h" />
<QtMoc Include="gamepatchsettingswidget.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="resource.h" />
<QtMoc Include="selectdiscdialog.h" />
@ -139,10 +139,7 @@
<QtUi Include="memorycardeditorwindow.ui">
<FileType>Document</FileType>
</QtUi>
<QtUi Include="cheatmanagerwindow.ui">
<FileType>Document</FileType>
</QtUi>
<QtUi Include="cheatcodeeditordialog.ui">
<QtUi Include="gamecheatcodeeditordialog.ui">
<FileType>Document</FileType>
</QtUi>
<QtUi Include="inputbindingdialog.ui">
@ -222,8 +219,6 @@
<ClCompile Include="$(IntDir)moc_autoupdaterdialog.cpp" />
<ClCompile Include="$(IntDir)moc_advancedsettingswidget.cpp" />
<ClCompile Include="$(IntDir)moc_biossettingswidget.cpp" />
<ClCompile Include="$(IntDir)moc_cheatmanagerwindow.cpp" />
<ClCompile Include="$(IntDir)moc_cheatcodeeditordialog.cpp" />
<ClCompile Include="$(IntDir)moc_colorpickerbutton.cpp" />
<ClCompile Include="$(IntDir)moc_consolesettingswidget.cpp" />
<ClCompile Include="$(IntDir)moc_controllerbindingwidgets.cpp" />
@ -233,10 +228,12 @@
<ClCompile Include="$(IntDir)moc_displaywidget.cpp" />
<ClCompile Include="$(IntDir)moc_emulationsettingswidget.cpp" />
<ClCompile Include="$(IntDir)moc_foldersettingswidget.cpp" />
<ClCompile Include="$(IntDir)moc_gamecheatsettingswidget.cpp" />
<ClCompile Include="$(IntDir)moc_gamelistmodel.cpp" />
<ClCompile Include="$(IntDir)moc_gamelistrefreshthread.cpp" />
<ClCompile Include="$(IntDir)moc_gamelistsettingswidget.cpp" />
<ClCompile Include="$(IntDir)moc_gamelistwidget.cpp" />
<ClCompile Include="$(IntDir)moc_gamepatchsettingswidget.cpp" />
<ClCompile Include="$(IntDir)moc_gamesummarywidget.cpp" />
<ClCompile Include="$(IntDir)moc_graphicssettingswidget.cpp" />
<ClCompile Include="$(IntDir)moc_debuggermodels.cpp" />
@ -342,6 +339,18 @@
<QtUi Include="texturereplacementsettingsdialog.ui">
<FileType>Document</FileType>
</QtUi>
<QtUi Include="gamecheatsettingswidget.ui">
<FileType>Document</FileType>
</QtUi>
<QtUi Include="gamepatchsettingswidget.ui">
<FileType>Document</FileType>
</QtUi>
<QtUi Include="gamepatchdetailswidget.ui">
<FileType>Document</FileType>
</QtUi>
<QtUi Include="gamecheatcodechoiceeditordialog.ui">
<FileType>Document</FileType>
</QtUi>
<None Include="translations\duckstation-qt_es-es.ts" />
<None Include="translations\duckstation-qt_tr.ts" />
</ItemGroup>
@ -395,7 +404,6 @@
<PreprocessorDefinitions>QT_NO_EXCEPTIONS=1;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<DisableSpecificWarnings>4127;%(DisableSpecificWarnings)</DisableSpecificWarnings>
<AdditionalIncludeDirectories>%(AdditionalIncludeDirectories);$(SolutionDir)dep\minizip\include</AdditionalIncludeDirectories>
<!-- Qt relies on RTTI for assertions in Debug builds. -->
<RuntimeTypeInfo Condition="$(Configuration.Contains(Debug))">true</RuntimeTypeInfo>
<AdditionalOptions Condition="$(Configuration.Contains(Clang)) And $(Configuration.Contains(Debug))">%(AdditionalOptions) /clang:-frtti</AdditionalOptions>

View File

@ -23,8 +23,6 @@
<ClCompile Include="biossettingswidget.cpp" />
<ClCompile Include="memorycardeditorwindow.cpp" />
<ClCompile Include="postprocessingsettingswidget.cpp" />
<ClCompile Include="cheatmanagerwindow.cpp" />
<ClCompile Include="cheatcodeeditordialog.cpp" />
<ClCompile Include="debuggerwindow.cpp" />
<ClCompile Include="debuggermodels.cpp" />
<ClCompile Include="memoryviewwidget.cpp" />
@ -65,12 +63,6 @@
<ClCompile Include="$(IntDir)moc_biossettingswidget.cpp">
<Filter>moc</Filter>
</ClCompile>
<ClCompile Include="$(IntDir)moc_cheatcodeeditordialog.cpp">
<Filter>moc</Filter>
</ClCompile>
<ClCompile Include="$(IntDir)moc_cheatmanagerwindow.cpp">
<Filter>moc</Filter>
</ClCompile>
<ClCompile Include="$(IntDir)moc_colorpickerbutton.cpp">
<Filter>moc</Filter>
</ClCompile>
@ -175,6 +167,14 @@
</ClCompile>
<ClCompile Include="vcruntimecheck.cpp" />
<ClCompile Include="qtthemes.cpp" />
<ClCompile Include="gamecheatsettingswidget.cpp" />
<ClCompile Include="gamepatchsettingswidget.cpp" />
<ClCompile Include="$(IntDir)moc_gamepatchsettingswidget.cpp">
<Filter>moc</Filter>
</ClCompile>
<ClCompile Include="$(IntDir)moc_gamecheatsettingswidget.cpp">
<Filter>moc</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="qtutils.h" />
@ -216,8 +216,6 @@
<QtMoc Include="biossettingswidget.h" />
<QtMoc Include="memorycardeditorwindow.h" />
<QtMoc Include="postprocessingsettingswidget.h" />
<QtMoc Include="cheatmanagerwindow.h" />
<QtMoc Include="cheatcodeeditordialog.h" />
<QtMoc Include="debuggermodels.h" />
<QtMoc Include="debuggerwindow.h" />
<QtMoc Include="memoryviewwidget.h" />
@ -237,6 +235,8 @@
<QtMoc Include="graphicssettingswidget.h" />
<QtMoc Include="memoryscannerwindow.h" />
<QtMoc Include="selectdiscdialog.h" />
<QtMoc Include="gamecheatsettingswidget.h" />
<QtMoc Include="gamepatchsettingswidget.h" />
</ItemGroup>
<ItemGroup>
<QtUi Include="consolesettingswidget.ui" />
@ -252,8 +252,7 @@
<QtUi Include="biossettingswidget.ui" />
<QtUi Include="postprocessingchainconfigwidget.ui" />
<QtUi Include="memorycardeditorwindow.ui" />
<QtUi Include="cheatmanagerwindow.ui" />
<QtUi Include="cheatcodeeditordialog.ui" />
<QtUi Include="gamecheatcodeeditordialog.ui" />
<QtUi Include="emulationsettingswidget.ui" />
<QtUi Include="achievementsettingswidget.ui" />
<QtUi Include="achievementlogindialog.ui" />
@ -285,6 +284,10 @@
<QtUi Include="controllerbindingwidget_justifier.ui" />
<QtUi Include="selectdiscdialog.ui" />
<QtUi Include="texturereplacementsettingsdialog.ui" />
<QtUi Include="gamepatchsettingswidget.ui" />
<QtUi Include="gamecheatsettingswidget.ui" />
<QtUi Include="gamepatchdetailswidget.ui" />
<QtUi Include="gamecheatcodechoiceeditordialog.ui" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="duckstation-qt.rc" />

View File

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>GameCheatCodeChoiceEditorDialog</class>
<widget class="QWidget" name="GameCheatCodeChoiceEditorDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>231</height>
</rect>
</property>
<property name="windowTitle">
<string>Cheat Choice Editor</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTreeWidget" name="optionList">
<column>
<property name="text">
<string>Name</string>
</property>
</column>
<column>
<property name="text">
<string>Value</string>
</property>
</column>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QToolButton" name="add">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Add Cheat</string>
</property>
<property name="icon">
<iconset theme="add-line"/>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonStyle::ToolButtonIconOnly</enum>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="remove">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Remove Cheat</string>
</property>
<property name="icon">
<iconset theme="minus-line"/>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonStyle::ToolButtonIconOnly</enum>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Save</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,142 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>GameCheatCodeEditorDialog</class>
<widget class="QDialog" name="GameCheatCodeEditorDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>491</width>
<height>318</height>
</rect>
</property>
<property name="windowTitle">
<string>Cheat Code Editor</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="nameLabel">
<property name="text">
<string>Name:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="name"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="descriptionLabel">
<property name="text">
<string>Description:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPlainTextEdit" name="description">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>48</height>
</size>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="groupLabel">
<property name="text">
<string>Group:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="group"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="typeLabel">
<property name="text">
<string>Type:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="type"/>
</item>
<item row="4" column="0">
<widget class="QLabel" name="activationLabel">
<property name="text">
<string>Activation:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QComboBox" name="activation"/>
</item>
<item row="6" column="0" colspan="2">
<widget class="QPlainTextEdit" name="instructions"/>
</item>
<item row="7" column="0" colspan="2">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Save</set>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Options:</string>
</property>
</widget>
</item>
<item row="5" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QComboBox" name="optionsType">
<item>
<property name="text">
<string>None</string>
</property>
</item>
<item>
<property name="text">
<string>Choice</string>
</property>
</item>
<item>
<property name="text">
<string>Range</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QSpinBox" name="rangeMin">
<property name="maximum">
<number>65535</number>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="rangeMax">
<property name="maximum">
<number>65535</number>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="editChoice">
<property name="text">
<string>Edit Choices...</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,948 @@
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#include "gamecheatsettingswidget.h"
#include "mainwindow.h"
#include "qthost.h"
#include "qtutils.h"
#include "settingswindow.h"
#include "settingwidgetbinder.h"
#include "core/cheats.h"
#include "common/error.h"
#include "common/string_util.h"
#include "fmt/format.h"
#include <QtCore/QSignalBlocker>
#include <QtGui/QPainter>
#include <QtWidgets/QInputDialog>
#include <QtWidgets/QStyledItemDelegate>
namespace {
class CheatListOptionDelegate : public QStyledItemDelegate
{
public:
CheatListOptionDelegate(GameCheatSettingsWidget* parent, QTreeWidget* treeview);
QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const override;
void setEditorData(QWidget* editor, const QModelIndex& index) const override;
void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override;
void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override;
private:
std::string getCodeNameForRow(const QModelIndex& index) const;
const Cheats::CodeInfo* getCodeInfoForRow(const QModelIndex& index) const;
GameCheatSettingsWidget* m_parent;
QTreeWidget* m_treeview;
};
}; // namespace
CheatListOptionDelegate::CheatListOptionDelegate(GameCheatSettingsWidget* parent, QTreeWidget* treeview)
: QStyledItemDelegate(parent), m_parent(parent), m_treeview(treeview)
{
}
std::string CheatListOptionDelegate::getCodeNameForRow(const QModelIndex& index) const
{
return index.siblingAtColumn(0).data(Qt::UserRole).toString().toStdString();
}
const Cheats::CodeInfo* CheatListOptionDelegate::getCodeInfoForRow(const QModelIndex& index) const
{
return m_parent->getCodeInfo(getCodeNameForRow(index));
}
QWidget* CheatListOptionDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& option,
const QModelIndex& index) const
{
// only edit the value, don't want the title becoming editable
if (index.column() != 1)
return nullptr;
const QVariant data = index.data(Qt::UserRole);
if (data.isNull())
return nullptr;
// if it's a uint, it's a range, otherwise string => combobox
if (data.typeId() == QMetaType::QString)
return new QComboBox(parent);
else if (data.typeId() == QMetaType::UInt)
return new QSpinBox(parent);
else
return QStyledItemDelegate::createEditor(parent, option, index);
}
void CheatListOptionDelegate::setEditorData(QWidget* editor, const QModelIndex& index) const
{
if (index.column() != 1)
return;
if (QComboBox* cb = qobject_cast<QComboBox*>(editor))
{
const Cheats::CodeInfo* ci = getCodeInfoForRow(index);
if (ci)
{
int current_index = 0;
const QString selected_name = index.data(Qt::UserRole).toString();
for (const Cheats::CodeOption& opt : ci->options)
{
const QString name = QString::fromStdString(opt.first);
cb->addItem(name, QVariant(static_cast<uint>(opt.second)));
if (name == selected_name)
cb->setCurrentIndex(current_index);
current_index++;
}
}
}
else if (QSpinBox* sb = qobject_cast<QSpinBox*>(editor))
{
const Cheats::CodeInfo* ci = getCodeInfoForRow(index);
if (ci)
{
sb->setMinimum(ci->option_range_start);
sb->setMaximum(ci->option_range_end);
sb->setValue(index.data(Qt::UserRole).toUInt());
}
}
else
{
return QStyledItemDelegate::setEditorData(editor, index);
}
}
void CheatListOptionDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const
{
if (index.column() != 1)
return;
if (QComboBox* cb = qobject_cast<QComboBox*>(editor))
{
const QString value = cb->currentText();
const Cheats::CodeInfo* ci = getCodeInfoForRow(index);
if (ci)
{
m_parent->setCodeOption(ci->name, ci->MapOptionNameToValue(value.toStdString()));
model->setData(index, value, Qt::UserRole);
}
}
else if (QSpinBox* sb = qobject_cast<QSpinBox*>(editor))
{
const u32 value = static_cast<u32>(sb->value());
m_parent->setCodeOption(getCodeNameForRow(index), value);
model->setData(index, static_cast<uint>(value), Qt::UserRole);
}
else
{
return QStyledItemDelegate::setModelData(editor, model, index);
}
}
void CheatListOptionDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option,
const QModelIndex& index) const
{
if (index.column() == 0)
{
// skip for editable rows
if (index.flags() & Qt::ItemIsEditable)
return QStyledItemDelegate::paint(painter, option, index);
// expand the width to full for those without options
QStyleOptionViewItem option_copy(option);
option_copy.rect.setWidth(option_copy.rect.width() + m_treeview->columnWidth(1));
return QStyledItemDelegate::paint(painter, option_copy, index);
}
else
{
if (!(index.flags() & Qt::ItemIsEditable))
{
// disable painting this column, so we can expand it (see above)
return;
}
// just draw the number or label as a string
const QVariant data = index.data(Qt::UserRole);
painter->drawText(option.rect, 0, data.toString());
}
}
GameCheatSettingsWidget::GameCheatSettingsWidget(SettingsWindow* dialog, QWidget* parent) : m_dialog(dialog)
{
m_ui.setupUi(this);
m_ui.cheatList->setItemDelegate(new CheatListOptionDelegate(this, m_ui.cheatList));
reloadList();
SettingsInterface* sif = m_dialog->getSettingsInterface();
// We don't use the binder here, because they're binary - either enabled, or not in the file.
m_ui.enableCheats->setChecked(sif->GetBoolValue("Cheats", "EnableCheats", false));
m_ui.loadDatabaseCheats->setChecked(sif->GetBoolValue("Cheats", "LoadCheatsFromDatabase", true));
connect(m_ui.enableCheats, &QCheckBox::checkStateChanged, this, &GameCheatSettingsWidget::onEnableCheatsChanged);
connect(m_ui.loadDatabaseCheats, &QCheckBox::checkStateChanged, this,
&GameCheatSettingsWidget::onLoadDatabaseCheatsChanged);
connect(m_ui.cheatList, &QTreeWidget::itemDoubleClicked, this,
&GameCheatSettingsWidget::onCheatListItemDoubleClicked);
connect(m_ui.cheatList, &QTreeWidget::customContextMenuRequested, this,
&GameCheatSettingsWidget::onCheatListContextMenuRequested);
connect(m_ui.cheatList, &QTreeWidget::itemChanged, this, &GameCheatSettingsWidget::onCheatListItemChanged);
connect(m_ui.add, &QToolButton::clicked, this, &GameCheatSettingsWidget::newCode);
connect(m_ui.remove, &QToolButton::clicked, this, &GameCheatSettingsWidget::onRemoveCodeClicked);
connect(m_ui.disableAll, &QToolButton::clicked, this, &GameCheatSettingsWidget::disableAllCheats);
connect(m_ui.reloadCheats, &QToolButton::clicked, this, &GameCheatSettingsWidget::onReloadClicked);
connect(m_ui.importCheats, &QPushButton::clicked, this, &GameCheatSettingsWidget::onImportClicked);
connect(m_ui.exportCheats, &QPushButton::clicked, this, &GameCheatSettingsWidget::onExportClicked);
}
GameCheatSettingsWidget::~GameCheatSettingsWidget() = default;
const Cheats::CodeInfo* GameCheatSettingsWidget::getCodeInfo(const std::string_view name) const
{
return Cheats::FindCodeInInfoList(m_codes, name);
}
void GameCheatSettingsWidget::setCodeOption(const std::string_view name, u32 value)
{
const Cheats::CodeInfo* info = getCodeInfo(name);
if (!info)
return;
m_dialog->getSettingsInterface()->SetUIntValue("Cheats", info->name.c_str(), value);
m_dialog->saveAndReloadGameSettings();
}
std::string GameCheatSettingsWidget::getPathForSavingCheats() const
{
// Check for the path without the hash first. If we have one of those, keep using it.
std::string path = Cheats::GetChtFilename(m_dialog->getGameSerial(), std::nullopt, true);
if (!FileSystem::FileExists(path.c_str()))
path = Cheats::GetChtFilename(m_dialog->getGameSerial(), m_dialog->getGameHash(), true);
return path;
}
QStringList GameCheatSettingsWidget::getGroupNames() const
{
std::vector<std::string_view> unique_prefixes = Cheats::GetCodeListUniquePrefixes(m_codes, false);
QStringList ret;
if (!unique_prefixes.empty())
{
ret.reserve(unique_prefixes.size());
for (const std::string_view& prefix : unique_prefixes)
ret.push_back(QtUtils::StringViewToQString(prefix));
}
return ret;
}
bool GameCheatSettingsWidget::hasCodeWithName(const std::string_view name) const
{
return (Cheats::FindCodeInInfoList(m_codes, name) != nullptr);
}
void GameCheatSettingsWidget::onEnableCheatsChanged(Qt::CheckState state)
{
if (state == Qt::Checked)
m_dialog->getSettingsInterface()->SetBoolValue("Cheats", "EnableCheats", true);
else
m_dialog->getSettingsInterface()->DeleteValue("Cheats", "EnableCheats");
m_dialog->saveAndReloadGameSettings();
}
void GameCheatSettingsWidget::onLoadDatabaseCheatsChanged(Qt::CheckState state)
{
// Default is enabled.
if (state == Qt::Checked)
m_dialog->getSettingsInterface()->DeleteValue("Cheats", "LoadCheatsFromDatabase");
else
m_dialog->getSettingsInterface()->SetBoolValue("Cheats", "LoadCheatsFromDatabase", false);
m_dialog->saveAndReloadGameSettings();
reloadList();
}
void GameCheatSettingsWidget::onCheatListItemDoubleClicked(QTreeWidgetItem* item, int column)
{
const QVariant item_data = item->data(0, Qt::UserRole);
if (!item_data.isValid())
return;
editCode(item_data.toString().toStdString());
}
void GameCheatSettingsWidget::onCheatListItemChanged(QTreeWidgetItem* item, int column)
{
const QVariant item_data = item->data(0, Qt::UserRole);
if (!item_data.isValid())
return;
std::string cheat_name = item_data.toString().toStdString();
const bool current_enabled =
(std::find(m_enabled_codes.begin(), m_enabled_codes.end(), cheat_name) != m_enabled_codes.end());
const bool current_checked = (item->checkState(0) == Qt::Checked);
if (current_enabled == current_checked)
return;
setCheatEnabled(std::move(cheat_name), current_checked, true);
}
void GameCheatSettingsWidget::onCheatListContextMenuRequested(const QPoint& pos)
{
Cheats::CodeInfo* selected = getSelectedCode();
const std::string selected_code = selected ? selected->name : std::string();
QMenu context_menu(m_ui.cheatList);
QAction* add = context_menu.addAction(QIcon::fromTheme("add-line"), tr("Add Cheat..."));
connect(add, &QAction::triggered, this, &GameCheatSettingsWidget::newCode);
QAction* edit = context_menu.addAction(QIcon::fromTheme("mag-line"), tr("Edit Cheat..."));
edit->setEnabled(selected != nullptr);
connect(edit, &QAction::triggered, this, [this, &selected_code]() { editCode(selected_code); });
QAction* remove = context_menu.addAction(QIcon::fromTheme("minus-line"), tr("Remove Cheat"));
remove->setEnabled(selected != nullptr);
connect(remove, &QAction::triggered, this, [this, &selected_code]() { removeCode(selected_code, true); });
context_menu.addSeparator();
QAction* disable_all = context_menu.addAction(QIcon::fromTheme("chat-off-line"), tr("Disable All Cheats"));
connect(disable_all, &QAction::triggered, this, &GameCheatSettingsWidget::disableAllCheats);
QAction* reload = context_menu.addAction(QIcon::fromTheme("refresh-line"), tr("Reload Cheats"));
connect(reload, &QAction::triggered, this, &GameCheatSettingsWidget::onReloadClicked);
context_menu.exec(m_ui.cheatList->mapToGlobal(pos));
}
void GameCheatSettingsWidget::onRemoveCodeClicked()
{
Cheats::CodeInfo* selected = getSelectedCode();
if (!selected)
return;
removeCode(selected->name, true);
}
void GameCheatSettingsWidget::onReloadClicked()
{
reloadList();
g_emu_thread->reloadCheats(true, false, true, true);
}
bool GameCheatSettingsWidget::shouldLoadFromDatabase() const
{
return m_dialog->getSettingsInterface()->GetBoolValue("Cheats", "LoadCheatsFromDatabase", true);
}
Cheats::CodeInfo* GameCheatSettingsWidget::getSelectedCode()
{
const QList<QTreeWidgetItem*> selected = m_ui.cheatList->selectedItems();
if (selected.size() != 1)
return nullptr;
const QVariant item_data = selected[0]->data(0, Qt::UserRole);
if (!item_data.isValid())
return nullptr;
return Cheats::FindCodeInInfoList(m_codes, item_data.toString().toStdString());
}
void GameCheatSettingsWidget::disableAllCheats()
{
setStateForAll(false);
}
void GameCheatSettingsWidget::resizeEvent(QResizeEvent* event)
{
QWidget::resizeEvent(event);
QtUtils::ResizeColumnsForTreeView(m_ui.cheatList, {-1, 150});
}
void GameCheatSettingsWidget::setCheatEnabled(std::string name, bool enabled, bool save_and_reload_settings)
{
SettingsInterface* si = m_dialog->getSettingsInterface();
const auto it = std::find(m_enabled_codes.begin(), m_enabled_codes.end(), name);
if (enabled)
{
si->AddToStringList(Cheats::CHEATS_CONFIG_SECTION, Cheats::PATCH_ENABLE_CONFIG_KEY, name.c_str());
if (it == m_enabled_codes.end())
m_enabled_codes.push_back(std::move(name));
}
else
{
si->RemoveFromStringList(Cheats::CHEATS_CONFIG_SECTION, Cheats::PATCH_ENABLE_CONFIG_KEY, name.c_str());
if (it != m_enabled_codes.end())
m_enabled_codes.erase(it);
}
if (save_and_reload_settings)
m_dialog->saveAndReloadGameSettings();
}
void GameCheatSettingsWidget::setStateForAll(bool enabled)
{
QSignalBlocker sb(m_ui.cheatList);
setStateRecursively(nullptr, enabled);
m_dialog->saveAndReloadGameSettings();
}
void GameCheatSettingsWidget::setStateRecursively(QTreeWidgetItem* parent, bool enabled)
{
const int count = parent ? parent->childCount() : m_ui.cheatList->topLevelItemCount();
for (int i = 0; i < count; i++)
{
QTreeWidgetItem* item = parent ? parent->child(i) : m_ui.cheatList->topLevelItem(i);
const QVariant item_data = item->data(0, Qt::UserRole);
if (item_data.isValid())
{
if ((item->checkState(0) == Qt::Checked) != enabled)
{
item->setCheckState(0, enabled ? Qt::Checked : Qt::Unchecked);
setCheatEnabled(item_data.toString().toStdString(), enabled, false);
}
}
else
{
setStateRecursively(item, enabled);
}
}
}
void GameCheatSettingsWidget::reloadList()
{
// Show all hashes, since the ini is shared.
m_codes = Cheats::GetCodeInfoList(m_dialog->getGameSerial(), std::nullopt, true, shouldLoadFromDatabase(), true);
m_enabled_codes =
m_dialog->getSettingsInterface()->GetStringList(Cheats::CHEATS_CONFIG_SECTION, Cheats::PATCH_ENABLE_CONFIG_KEY);
m_parent_map.clear();
while (m_ui.cheatList->topLevelItemCount() > 0)
delete m_ui.cheatList->takeTopLevelItem(0);
for (const Cheats::CodeInfo& ci : m_codes)
{
const bool enabled = (std::find(m_enabled_codes.begin(), m_enabled_codes.end(), ci.name) != m_enabled_codes.end());
const std::string_view parent_part = ci.GetNameParentPart();
QTreeWidgetItem* parent = getTreeWidgetParent(parent_part);
QTreeWidgetItem* item = new QTreeWidgetItem();
populateTreeWidgetItem(item, ci, enabled);
if (parent)
parent->addChild(item);
else
m_ui.cheatList->addTopLevelItem(item);
}
// Hide root indicator when there's no groups, frees up some whitespace.
m_ui.cheatList->setRootIsDecorated(!m_parent_map.empty());
}
void GameCheatSettingsWidget::onImportClicked()
{
QMenu menu(this);
connect(menu.addAction(tr("From File...")), &QAction::triggered, this,
&GameCheatSettingsWidget::onImportFromFileTriggered);
connect(menu.addAction(tr("From Text...")), &QAction::triggered, this,
&GameCheatSettingsWidget::onImportFromTextTriggered);
menu.exec(QCursor::pos());
}
void GameCheatSettingsWidget::onImportFromFileTriggered()
{
const QString filter(tr("PCSXR/Libretro Cheat Files (*.cht *.txt);;All Files (*.*)"));
const QString filename =
QDir::toNativeSeparators(QFileDialog::getOpenFileName(this, tr("Import Cheats"), QString(), filter));
if (filename.isEmpty())
return;
Error error;
const std::optional<std::string> file_contents = FileSystem::ReadFileToString(filename.toStdString().c_str(), &error);
if (!file_contents.has_value())
{
QMessageBox::critical(this, tr("Error"),
tr("Failed to read file:\n%1").arg(QString::fromStdString(error.GetDescription())));
return;
}
importCodes(file_contents.value());
}
void GameCheatSettingsWidget::onImportFromTextTriggered()
{
const QString text = QInputDialog::getMultiLineText(this, tr("Import Cheats"), tr("Cheat File Text:"));
if (text.isEmpty())
return;
importCodes(text.toStdString());
}
void GameCheatSettingsWidget::importCodes(const std::string& file_contents)
{
Error error;
Cheats::CodeInfoList new_codes;
if (!Cheats::ImportCodesFromString(&new_codes, file_contents, Cheats::FileFormat::Unknown, true, &error))
{
QMessageBox::critical(this, tr("Error"),
tr("Failed to parse file:\n%1").arg(QString::fromStdString(error.GetDescription())));
return;
}
Cheats::MergeCheatList(&m_codes, std::move(new_codes));
if (!Cheats::SaveCodesToFile(getPathForSavingCheats().c_str(), m_codes, &error))
{
QMessageBox::critical(this, tr("Error"),
tr("Failed to save file:\n%1").arg(QString::fromStdString(error.GetDescription())));
}
reloadList();
}
void GameCheatSettingsWidget::newCode()
{
Cheats::CodeInfo new_code;
CheatCodeEditorDialog dlg(this, &new_code, getGroupNames());
if (!dlg.exec())
{
// cancelled
return;
}
// no need to reload cheats yet, it's not active. just refresh the list
reloadList();
}
void GameCheatSettingsWidget::editCode(const std::string_view code_name)
{
Cheats::CodeInfo* code = Cheats::FindCodeInInfoList(m_codes, code_name);
if (!code)
return;
CheatCodeEditorDialog dlg(this, code, getGroupNames());
if (!dlg.exec())
{
// no changes
return;
}
reloadList();
g_emu_thread->reloadCheats(true, true, false, true);
}
void GameCheatSettingsWidget::removeCode(const std::string_view code_name, bool confirm)
{
Cheats::CodeInfo* code = Cheats::FindCodeInInfoList(m_codes, code_name);
if (!code)
return;
if (code->from_database)
{
QMessageBox::critical(this, tr("Error"),
tr("This code is from the built-in cheat database, and cannot be removed. To hide this code, "
"uncheck the \"Load Database Cheats\" option."));
return;
}
if (QMessageBox::question(this, tr("Confirm Removal"),
tr("You are removing the code named '%1'. You cannot undo this action, are you sure you "
"wish to delete this code?")) != QMessageBox::Yes)
{
return;
}
Error error;
if (!Cheats::UpdateCodeInFile(getPathForSavingCheats().c_str(), code->name, nullptr, &error))
{
QMessageBox::critical(this, tr("Error"),
tr("Failed to save file:\n%1").arg(QString::fromStdString(error.GetDescription())));
return;
}
reloadList();
g_emu_thread->reloadCheats(true, true, false, true);
}
void GameCheatSettingsWidget::onExportClicked()
{
const QString filter(tr("PCSXR Cheat Files (*.cht);;All Files (*.*)"));
const QString filename =
QDir::toNativeSeparators(QFileDialog::getSaveFileName(this, tr("Export Cheats"), QString(), filter));
if (filename.isEmpty())
return;
Error error;
if (!Cheats::ExportCodesToFile(filename.toStdString(), m_codes, &error))
{
QMessageBox::critical(this, tr("Error"),
tr("Failed to save cheat file:\n%1").arg(QString::fromStdString(error.GetDescription())));
}
}
QTreeWidgetItem* GameCheatSettingsWidget::getTreeWidgetParent(const std::string_view parent)
{
if (parent.empty())
return nullptr;
auto it = m_parent_map.find(parent);
if (it != m_parent_map.end())
return it->second;
std::string_view this_part = parent;
QTreeWidgetItem* parent_to_this = nullptr;
const std::string_view::size_type pos = parent.rfind('\\');
if (pos != std::string::npos && pos != (parent.size() - 1))
{
// go up the chain until we find the real parent, then back down
parent_to_this = getTreeWidgetParent(parent.substr(0, pos));
this_part = parent.substr(pos + 1);
}
QTreeWidgetItem* item = new QTreeWidgetItem();
item->setText(0, QString::fromUtf8(this_part.data(), this_part.length()));
if (parent_to_this)
parent_to_this->addChild(item);
else
m_ui.cheatList->addTopLevelItem(item);
// Must be called after adding.
item->setExpanded(true);
m_parent_map.emplace(parent, item);
return item;
}
void GameCheatSettingsWidget::populateTreeWidgetItem(QTreeWidgetItem* item, const Cheats::CodeInfo& pi, bool enabled)
{
const std::string_view name_part = pi.GetNamePart();
item->setFlags(item->flags() | Qt::ItemIsUserCheckable | Qt::ItemNeverHasChildren);
item->setCheckState(0, enabled ? Qt::Checked : Qt::Unchecked);
item->setData(0, Qt::UserRole, QString::fromStdString(pi.name));
if (!pi.description.empty())
item->setToolTip(0, QString::fromStdString(pi.description));
if (!name_part.empty())
item->setText(0, QtUtils::StringViewToQString(name_part));
if (pi.HasOptionChoices())
{
// need to resolve the value back to a name
const std::string_view option_name =
pi.MapOptionValueToName(m_dialog->getSettingsInterface()->GetTinyStringValue("Cheats", pi.name.c_str()));
item->setData(1, Qt::UserRole, QtUtils::StringViewToQString(option_name));
item->setFlags(item->flags() | Qt::ItemIsEditable);
}
else if (pi.HasOptionRange())
{
const u32 value = m_dialog->getSettingsInterface()->GetUIntValue("Cheats", pi.name.c_str(), pi.option_range_start);
item->setData(1, Qt::UserRole, static_cast<uint>(value));
item->setFlags(item->flags() | Qt::ItemIsEditable);
}
}
CheatCodeEditorDialog::CheatCodeEditorDialog(GameCheatSettingsWidget* parent, Cheats::CodeInfo* code,
const QStringList& group_names)
: QDialog(parent), m_parent(parent), m_code(code)
{
m_ui.setupUi(this);
setupAdditionalUi(group_names);
fillUi();
connect(m_ui.group, &QComboBox::currentIndexChanged, this, &CheatCodeEditorDialog::onGroupSelectedIndexChanged);
connect(m_ui.optionsType, &QComboBox::currentIndexChanged, this, &CheatCodeEditorDialog::onOptionTypeChanged);
connect(m_ui.rangeMin, &QSpinBox::valueChanged, this, &CheatCodeEditorDialog::onRangeMinChanged);
connect(m_ui.rangeMax, &QSpinBox::valueChanged, this, &CheatCodeEditorDialog::onRangeMaxChanged);
connect(m_ui.editChoice, &QPushButton::clicked, this, &CheatCodeEditorDialog::onEditChoiceClicked);
connect(m_ui.buttonBox, &QDialogButtonBox::accepted, this, &CheatCodeEditorDialog::saveClicked);
connect(m_ui.buttonBox, &QDialogButtonBox::rejected, this, &CheatCodeEditorDialog::cancelClicked);
}
CheatCodeEditorDialog::~CheatCodeEditorDialog() = default;
void CheatCodeEditorDialog::onGroupSelectedIndexChanged(int index)
{
if (index != (m_ui.group->count() - 1))
return;
// new item...
const QString text = QInputDialog::getText(
this, tr("Enter Group Name"), tr("Enter name for the code group. Using backslashes (\\) will create sub-trees."));
// don't want this re-triggering
QSignalBlocker sb(m_ui.group);
if (text.isEmpty())
{
// cancelled...
m_ui.group->setCurrentIndex(0);
return;
}
const int existing_index = m_ui.group->findText(text);
if (existing_index >= 0)
{
m_ui.group->setCurrentIndex(existing_index);
return;
}
m_ui.group->insertItem(index, text);
m_ui.group->setCurrentIndex(index);
}
void CheatCodeEditorDialog::saveClicked()
{
std::string new_name = m_ui.name->text().toStdString();
if (new_name.empty())
{
QMessageBox::critical(this, tr("Error"), tr("Name cannot be empty."));
return;
}
std::string new_body = m_ui.instructions->toPlainText().toStdString();
if (new_body.empty())
{
QMessageBox::critical(this, tr("Error"), tr("Instructions cannot be empty."));
return;
}
// name actually includes the prefix
if (const int index = m_ui.group->currentIndex(); index != 0)
{
const std::string prefix = m_ui.group->currentText().toStdString();
if (!prefix.empty())
new_name = fmt::format("{}\\{}", prefix, new_name);
}
// if the name has changed, then we need to make sure it hasn't already been used
if (new_name != m_code->name && m_parent->hasCodeWithName(new_name))
{
QMessageBox::critical(this, tr("Error"),
tr("A code with the name '%1' already exists.").arg(QString::fromStdString(new_name)));
return;
}
std::string old_name = std::move(m_code->name);
// cheats coming from the database need to be copied into the user's file
if (m_code->from_database)
{
m_code->from_database = false;
old_name.clear();
}
m_code->name = std::move(new_name);
m_code->description = m_ui.description->toPlainText().replace('\n', ' ').toStdString();
m_code->type = static_cast<Cheats::CodeType>(m_ui.type->currentIndex());
m_code->activation = static_cast<Cheats::CodeActivation>(m_ui.activation->currentIndex());
m_code->body = std::move(new_body);
m_code->option_range_start = 0;
m_code->option_range_end = 0;
m_code->options = {};
if (m_ui.optionsType->currentIndex() == 1)
{
// choices
m_code->options = std::move(m_new_options);
}
else if (m_ui.optionsType->currentIndex() == 2)
{
// range
m_code->option_range_start = static_cast<u16>(m_ui.rangeMin->value());
m_code->option_range_end = static_cast<u16>(m_ui.rangeMax->value());
}
std::string path = m_parent->getPathForSavingCheats();
Error error;
if (!Cheats::UpdateCodeInFile(path.c_str(), old_name, m_code, &error))
{
QMessageBox::critical(this, tr("Error"),
tr("Failed to save cheat code:\n%1").arg(QString::fromStdString(error.GetDescription())));
}
done(1);
}
void CheatCodeEditorDialog::cancelClicked()
{
done(0);
}
void CheatCodeEditorDialog::onOptionTypeChanged(int index)
{
m_ui.editChoice->setVisible(index == 1);
m_ui.rangeMin->setVisible(index == 2);
m_ui.rangeMax->setVisible(index == 2);
}
void CheatCodeEditorDialog::onRangeMinChanged(int value)
{
m_ui.rangeMax->setValue(std::max(m_ui.rangeMax->value(), value));
}
void CheatCodeEditorDialog::onRangeMaxChanged(int value)
{
m_ui.rangeMin->setValue(std::min(m_ui.rangeMin->value(), value));
}
void CheatCodeEditorDialog::onEditChoiceClicked()
{
GameCheatCodeChoiceEditorDialog dlg(this, m_new_options);
if (dlg.exec())
m_new_options = dlg.getNewOptions();
}
void CheatCodeEditorDialog::setupAdditionalUi(const QStringList& group_names)
{
for (u32 i = 0; i < static_cast<u32>(Cheats::CodeType::Count); i++)
m_ui.type->addItem(Cheats::GetTypeDisplayName(static_cast<Cheats::CodeType>(i)));
for (u32 i = 0; i < static_cast<u32>(Cheats::CodeActivation::Count); i++)
m_ui.activation->addItem(Cheats::GetActivationDisplayName(static_cast<Cheats::CodeActivation>(i)));
m_ui.group->addItem(tr("Ungrouped"));
if (!group_names.isEmpty())
m_ui.group->addItems(group_names);
m_ui.group->addItem(tr("New..."));
}
void CheatCodeEditorDialog::fillUi()
{
m_ui.name->setText(QtUtils::StringViewToQString(m_code->GetNamePart()));
m_ui.description->setPlainText(QString::fromStdString(m_code->description));
const std::string_view group = m_code->GetNameParentPart();
if (group.empty())
{
// ungrouped is always first
m_ui.group->setCurrentIndex(0);
}
else
{
const QString group_qstr(QtUtils::StringViewToQString(group));
int index = m_ui.group->findText(group_qstr);
if (index < 0)
{
// shouldn't happen...
index = m_ui.group->count() - 1;
m_ui.group->insertItem(index, group_qstr);
}
m_ui.group->setCurrentIndex(index);
}
m_ui.type->setCurrentIndex(static_cast<int>(m_code->type));
m_ui.activation->setCurrentIndex(static_cast<int>(m_code->activation));
m_ui.instructions->setPlainText(QString::fromStdString(m_code->body));
m_ui.rangeMin->setValue(static_cast<int>(m_code->option_range_start));
m_ui.rangeMax->setValue(static_cast<int>(m_code->option_range_end));
m_new_options = m_code->options;
m_ui.optionsType->setCurrentIndex(m_code->HasOptionRange() ? 2 : (m_code->HasOptionChoices() ? 1 : 0));
onOptionTypeChanged(m_ui.optionsType->currentIndex());
}
GameCheatCodeChoiceEditorDialog::GameCheatCodeChoiceEditorDialog(QWidget* parent, const Cheats::CodeOptionList& options)
: QDialog(parent)
{
m_ui.setupUi(this);
connect(m_ui.add, &QToolButton::clicked, this, &GameCheatCodeChoiceEditorDialog::onAddClicked);
connect(m_ui.remove, &QToolButton::clicked, this, &GameCheatCodeChoiceEditorDialog::onRemoveClicked);
connect(m_ui.buttonBox, &QDialogButtonBox::accepted, this, &GameCheatCodeChoiceEditorDialog::onSaveClicked);
connect(m_ui.buttonBox, &QDialogButtonBox::rejected, this, &CheatCodeEditorDialog::reject);
m_ui.optionList->setRootIsDecorated(false);
for (const Cheats::CodeOption& opt : options)
{
QTreeWidgetItem* item = new QTreeWidgetItem();
item->setFlags(item->flags() | Qt::ItemIsEditable);
item->setText(0, QString::fromStdString(opt.first));
item->setText(1, QString::number(opt.second));
m_ui.optionList->addTopLevelItem(item);
}
}
GameCheatCodeChoiceEditorDialog::~GameCheatCodeChoiceEditorDialog() = default;
void GameCheatCodeChoiceEditorDialog::resizeEvent(QResizeEvent* event)
{
QDialog::resizeEvent(event);
QtUtils::ResizeColumnsForTreeView(m_ui.optionList, {-1, 150});
}
void GameCheatCodeChoiceEditorDialog::onAddClicked()
{
QTreeWidgetItem* item = new QTreeWidgetItem();
item->setFlags(item->flags() | Qt::ItemIsEditable);
item->setText(0, QStringLiteral("Option %1").arg(m_ui.optionList->topLevelItemCount()));
item->setText(1, QStringLiteral("0"));
m_ui.optionList->addTopLevelItem(item);
}
void GameCheatCodeChoiceEditorDialog::onRemoveClicked()
{
const QList<QTreeWidgetItem*> items = m_ui.optionList->selectedItems();
for (QTreeWidgetItem* item : items)
{
const int index = m_ui.optionList->indexOfTopLevelItem(item);
if (index >= 0)
delete m_ui.optionList->takeTopLevelItem(index);
}
}
void GameCheatCodeChoiceEditorDialog::onSaveClicked()
{
// validate the data
const int count = m_ui.optionList->topLevelItemCount();
if (count == 0)
{
QMessageBox::critical(this, tr("Error"), tr("At least one option must be defined."));
return;
}
for (int i = 0; i < count; i++)
{
const QTreeWidgetItem* it = m_ui.optionList->topLevelItem(i);
const QString this_name = it->text(0);
for (int j = 0; j < count; j++)
{
if (i == j)
continue;
if (m_ui.optionList->topLevelItem(j)->text(0) == this_name)
{
QMessageBox::critical(this, tr("Error"), tr("The option '%1' is defined twice.").arg(this_name));
return;
}
}
// should be a parseable number
const QString this_value = it->text(1);
if (bool ok; this_value.toUInt(&ok), !ok)
{
QMessageBox::critical(this, tr("Error"),
tr("The option '%1' does not have a valid value. It must be a number.").arg(this_name));
return;
}
}
accept();
}
Cheats::CodeOptionList GameCheatCodeChoiceEditorDialog::getNewOptions() const
{
Cheats::CodeOptionList ret;
const int count = m_ui.optionList->topLevelItemCount();
ret.reserve(static_cast<size_t>(count));
for (int i = 0; i < count; i++)
{
const QTreeWidgetItem* it = m_ui.optionList->topLevelItem(i);
ret.emplace_back(it->text(0).toStdString(), it->text(1).toUInt());
}
return ret;
}

View File

@ -0,0 +1,131 @@
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#pragma once
#include "ui_gamecheatcodechoiceeditordialog.h"
#include "ui_gamecheatcodeeditordialog.h"
#include "ui_gamecheatsettingswidget.h"
#include "core/cheats.h"
#include "common/heterogeneous_containers.h"
#include <QtCore/QStringList>
#include <QtWidgets/QDialog>
#include <QtWidgets/QWidget>
#include <string>
#include <string_view>
#include <vector>
namespace GameList {
struct Entry;
}
class SettingsWindow;
class GameCheatSettingsWidget : public QWidget
{
Q_OBJECT
public:
GameCheatSettingsWidget(SettingsWindow* dialog, QWidget* parent);
~GameCheatSettingsWidget() override;
const Cheats::CodeInfo* getCodeInfo(const std::string_view name) const;
void setCodeOption(const std::string_view name, u32 value);
std::string getPathForSavingCheats() const;
QStringList getGroupNames() const;
bool hasCodeWithName(const std::string_view name) const;
void disableAllCheats();
protected:
void resizeEvent(QResizeEvent* event) override;
private Q_SLOTS:
void onEnableCheatsChanged(Qt::CheckState state);
void onLoadDatabaseCheatsChanged(Qt::CheckState state);
void onCheatListItemDoubleClicked(QTreeWidgetItem* item, int column);
void onCheatListItemChanged(QTreeWidgetItem* item, int column);
void onCheatListContextMenuRequested(const QPoint& pos);
void onRemoveCodeClicked();
void onReloadClicked();
void onImportClicked();
void onImportFromFileTriggered();
void onImportFromTextTriggered();
void onExportClicked();
void reloadList();
private:
bool shouldLoadFromDatabase() const;
Cheats::CodeInfo* getSelectedCode();
QTreeWidgetItem* getTreeWidgetParent(const std::string_view parent);
void populateTreeWidgetItem(QTreeWidgetItem* item, const Cheats::CodeInfo& pi, bool enabled);
void setCheatEnabled(std::string name, bool enabled, bool save_and_reload_settings);
void setStateForAll(bool enabled);
void setStateRecursively(QTreeWidgetItem* parent, bool enabled);
void importCodes(const std::string& file_contents);
void newCode();
void editCode(const std::string_view code_name);
void removeCode(const std::string_view code_name, bool confirm);
Ui::GameCheatSettingsWidget m_ui;
SettingsWindow* m_dialog;
UnorderedStringMap<QTreeWidgetItem*> m_parent_map;
Cheats::CodeInfoList m_codes;
std::vector<std::string> m_enabled_codes;
};
class CheatCodeEditorDialog : public QDialog
{
Q_OBJECT
public:
CheatCodeEditorDialog(GameCheatSettingsWidget* parent, Cheats::CodeInfo* code, const QStringList& group_names);
~CheatCodeEditorDialog() override;
private Q_SLOTS:
void onGroupSelectedIndexChanged(int index);
void saveClicked();
void cancelClicked();
void onOptionTypeChanged(int index);
void onRangeMinChanged(int value);
void onRangeMaxChanged(int value);
void onEditChoiceClicked();
private:
void setupAdditionalUi(const QStringList& group_names);
void fillUi();
GameCheatSettingsWidget* m_parent;
Ui::GameCheatCodeEditorDialog m_ui;
Cheats::CodeInfo* m_code;
Cheats::CodeOptionList m_new_options;
};
class GameCheatCodeChoiceEditorDialog : public QDialog
{
Q_OBJECT
public:
GameCheatCodeChoiceEditorDialog(QWidget* parent, const Cheats::CodeOptionList& options);
~GameCheatCodeChoiceEditorDialog() override;
Cheats::CodeOptionList getNewOptions() const;
protected:
void resizeEvent(QResizeEvent* event) override;
private Q_SLOTS:
void onAddClicked();
void onRemoveClicked();
void onSaveClicked();
private:
Ui::GameCheatCodeChoiceEditorDialog m_ui;
};

View File

@ -0,0 +1,175 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>GameCheatSettingsWidget</class>
<widget class="QWidget" name="GameCheatSettingsWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>821</width>
<height>401</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2" stretch="1,0,0,0,0">
<item>
<widget class="QCheckBox" name="enableCheats">
<property name="text">
<string>Enable Cheats</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="add">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Add Cheat</string>
</property>
<property name="icon">
<iconset theme="add-line"/>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonStyle::ToolButtonIconOnly</enum>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="remove">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Remove Cheat</string>
</property>
<property name="icon">
<iconset theme="minus-line"/>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonStyle::ToolButtonIconOnly</enum>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="disableAll">
<property name="toolTip">
<string>Disable All Cheats</string>
</property>
<property name="icon">
<iconset theme="chat-off-line"/>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="reloadCheats">
<property name="toolTip">
<string>Reload Cheats</string>
</property>
<property name="icon">
<iconset theme="refresh-line"/>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QTreeWidget" name="cheatList">
<property name="contextMenuPolicy">
<enum>Qt::ContextMenuPolicy::CustomContextMenu</enum>
</property>
<property name="verticalScrollBarPolicy">
<enum>Qt::ScrollBarPolicy::ScrollBarAlwaysOn</enum>
</property>
<property name="editTriggers">
<set>QAbstractItemView::EditTrigger::AllEditTriggers</set>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SelectionMode::SingleSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectionBehavior::SelectRows</enum>
</property>
<property name="headerHidden">
<bool>true</bool>
</property>
<column>
<property name="text">
<string>Name</string>
</property>
</column>
<column>
<property name="text">
<string>Value</string>
</property>
</column>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QCheckBox" name="loadDatabaseCheats">
<property name="text">
<string>Load Database Cheats</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="importCheats">
<property name="text">
<string>Import...</string>
</property>
<property name="icon">
<iconset theme="import-line"/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="exportCheats">
<property name="text">
<string>Export...</string>
</property>
<property name="icon">
<iconset theme="export-line"/>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>GamePatchDetailsWidget</class>
<widget class="QWidget" name="GamePatchDetailsWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>541</width>
<height>112</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,1">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="name">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Patch Title</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QCheckBox" name="enabled">
<property name="text">
<string>Enabled</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="description">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:700;&quot;&gt;Author: &lt;/span&gt;Patch Author&lt;/p&gt;&lt;p&gt;Description would go here&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,125 @@
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#include "gamepatchsettingswidget.h"
#include "mainwindow.h"
#include "qthost.h"
#include "qtutils.h"
#include "settingswindow.h"
#include "settingwidgetbinder.h"
#include "core/cheats.h"
#include "common/assert.h"
#include <algorithm>
GamePatchDetailsWidget::GamePatchDetailsWidget(std::string name, const std::string& author,
const std::string& description, bool enabled, SettingsWindow* dialog,
QWidget* parent)
: QWidget(parent), m_dialog(dialog), m_name(name)
{
m_ui.setupUi(this);
m_ui.name->setText(QString::fromStdString(name));
m_ui.description->setText(
tr("<strong>Author: </strong>%1<br>%2")
.arg(author.empty() ? tr("Unknown") : QString::fromStdString(author))
.arg(description.empty() ? tr("No description provided.") : QString::fromStdString(description)));
DebugAssert(dialog->getSettingsInterface());
m_ui.enabled->setChecked(enabled);
connect(m_ui.enabled, &QCheckBox::checkStateChanged, this, &GamePatchDetailsWidget::onEnabledStateChanged);
}
GamePatchDetailsWidget::~GamePatchDetailsWidget() = default;
void GamePatchDetailsWidget::onEnabledStateChanged(int state)
{
SettingsInterface* si = m_dialog->getSettingsInterface();
if (state == Qt::Checked)
si->AddToStringList("Patches", "Enable", m_name.c_str());
else
si->RemoveFromStringList("Patches", "Enable", m_name.c_str());
si->Save();
g_emu_thread->reloadGameSettings();
}
GamePatchSettingsWidget::GamePatchSettingsWidget(SettingsWindow* dialog, QWidget* parent) : m_dialog(dialog)
{
m_ui.setupUi(this);
m_ui.scrollArea->setFrameShape(QFrame::WinPanel);
m_ui.scrollArea->setFrameShadow(QFrame::Sunken);
connect(m_ui.reload, &QPushButton::clicked, this, &GamePatchSettingsWidget::onReloadClicked);
connect(m_ui.disableAllPatches, &QPushButton::clicked, this, &GamePatchSettingsWidget::disableAllPatches);
reloadList();
}
GamePatchSettingsWidget::~GamePatchSettingsWidget() = default;
void GamePatchSettingsWidget::onReloadClicked()
{
reloadList();
// reload it on the emu thread too, so it picks up any changes
g_emu_thread->reloadCheats(true, false, true, true);
}
void GamePatchSettingsWidget::disableAllPatches()
{
SettingsInterface* sif = m_dialog->getSettingsInterface();
sif->RemoveSection(Cheats::PATCHES_CONFIG_SECTION);
m_dialog->saveAndReloadGameSettings();
reloadList();
}
void GamePatchSettingsWidget::reloadList()
{
const std::vector<Cheats::CodeInfo> patches =
Cheats::GetCodeInfoList(m_dialog->getGameSerial(), std::nullopt, false, true, true);
const std::vector<std::string> enabled_list =
m_dialog->getSettingsInterface()->GetStringList(Cheats::PATCHES_CONFIG_SECTION, Cheats::PATCH_ENABLE_CONFIG_KEY);
delete m_ui.scrollArea->takeWidget();
QWidget* container = new QWidget(m_ui.scrollArea);
QVBoxLayout* layout = new QVBoxLayout(container);
layout->setContentsMargins(0, 0, 0, 0);
if (!patches.empty())
{
bool first = true;
for (const Cheats::CodeInfo& pi : patches)
{
if (!first)
{
QFrame* frame = new QFrame(container);
frame->setFrameShape(QFrame::HLine);
frame->setFrameShadow(QFrame::Sunken);
layout->addWidget(frame);
}
else
{
first = false;
}
const bool enabled = (std::find(enabled_list.begin(), enabled_list.end(), pi.name) != enabled_list.end());
GamePatchDetailsWidget* it =
new GamePatchDetailsWidget(std::move(pi.name), pi.author, pi.description, enabled, m_dialog, container);
layout->addWidget(it);
}
}
else
{
QLabel* label = new QLabel(tr("There are no patches available for this game."), container);
layout->addWidget(label);
}
layout->addStretch(1);
m_ui.scrollArea->setWidget(container);
}

View File

@ -0,0 +1,54 @@
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#pragma once
#include "ui_gamepatchdetailswidget.h"
#include "ui_gamepatchsettingswidget.h"
#include <QtWidgets/QWidget>
namespace GameList {
struct Entry;
}
class SettingsWindow;
class GamePatchDetailsWidget : public QWidget
{
Q_OBJECT
public:
GamePatchDetailsWidget(std::string name, const std::string& author, const std::string& description, bool enabled,
SettingsWindow* dialog, QWidget* parent);
~GamePatchDetailsWidget();
private Q_SLOTS:
void onEnabledStateChanged(int state);
private:
Ui::GamePatchDetailsWidget m_ui;
SettingsWindow* m_dialog;
std::string m_name;
};
class GamePatchSettingsWidget : public QWidget
{
Q_OBJECT
public:
GamePatchSettingsWidget(SettingsWindow* dialog, QWidget* parent);
~GamePatchSettingsWidget();
public Q_SLOTS:
void disableAllPatches();
private Q_SLOTS:
void onReloadClicked();
private:
void reloadList();
Ui::GamePatchSettingsWidget m_ui;
SettingsWindow* m_dialog;
};

View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>GamePatchSettingsWidget</class>
<widget class="QWidget" name="GamePatchSettingsWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>766</width>
<height>392</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QScrollArea" name="scrollArea">
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarPolicy::ScrollBarAlwaysOff</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="disableAllPatches">
<property name="text">
<string>Disable All Patches</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="reload">
<property name="text">
<string>Reload Patches</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -5,7 +5,6 @@
#include "aboutdialog.h"
#include "achievementlogindialog.h"
#include "autoupdaterdialog.h"
#include "cheatmanagerwindow.h"
#include "coverdownloaddialog.h"
#include "debuggerwindow.h"
#include "displaywidget.h"
@ -23,6 +22,7 @@
#include "settingwidgetbinder.h"
#include "core/achievements.h"
#include "core/cheats.h"
#include "core/game_list.h"
#include "core/host.h"
#include "core/memory_card.h"
@ -787,7 +787,6 @@ void MainWindow::destroySubWindows()
{
QtUtils::CloseAndDeleteWindow(m_memory_scanner_window);
QtUtils::CloseAndDeleteWindow(m_debugger_window);
QtUtils::CloseAndDeleteWindow(m_cheat_manager_window);
QtUtils::CloseAndDeleteWindow(m_memory_card_editor_window);
QtUtils::CloseAndDeleteWindow(m_controller_settings_window);
QtUtils::CloseAndDeleteWindow(m_settings_window);
@ -998,125 +997,55 @@ void MainWindow::populateChangeDiscSubImageMenu(QMenu* menu, QActionGroup* actio
}
}
void MainWindow::updateCheatActionsVisibility()
{
// If the cheat system is disabled, put an action to enable it in place of the menu under System.
const bool cheats_enabled = Host::GetBoolSettingValue("Console", "EnableCheats", false);
m_ui.actionCheats->setVisible(!cheats_enabled);
m_ui.menuCheats->menuAction()->setVisible(cheats_enabled);
}
void MainWindow::onCheatsActionTriggered()
{
const bool cheats_enabled = Host::GetBoolSettingValue("Console", "EnableCheats", false);
if (cheats_enabled)
{
m_ui.menuCheats->exec(QCursor::pos());
return;
}
SystemLock lock(pauseAndLockSystem());
QMessageBox mb(this);
mb.setWindowTitle(tr("Enable Cheats"));
mb.setText(
tr("Using cheats can have unpredictable effects on games, causing crashes, graphical glitches, and corrupted "
"saves. By using the cheat manager, you agree that it is an unsupported configuration, and we will not "
"provide you with any assistance when games break.\n\nCheats persist through save states even after being "
"disabled, please remember to reset/reboot the game after turning off any codes.\n\nAre you sure you want "
"to continue?"));
mb.setIcon(QMessageBox::Warning);
QPushButton* global = mb.addButton(tr("Enable For All Games"), QMessageBox::DestructiveRole);
QPushButton* game = mb.addButton(tr("Enable For This Game"), QMessageBox::AcceptRole);
game->setEnabled(s_system_valid && !s_current_game_serial.isEmpty());
QPushButton* cancel = mb.addButton(tr("Cancel"), QMessageBox::RejectRole);
mb.setDefaultButton(cancel);
mb.setEscapeButton(cancel);
mb.exec();
if (mb.clickedButton() == global)
{
// enable globally
Host::SetBaseBoolSettingValue("Console", "EnableCheats", true);
Host::CommitBaseSettingChanges();
g_emu_thread->applySettings(false);
}
else if (mb.clickedButton() == game)
{
if (!SettingsWindow::setGameSettingsBoolForSerial(s_current_game_serial.toStdString(), "Console", "EnableCheats",
true))
{
QMessageBox::critical(this, tr("Error"), tr("Failed to enable cheats for %1.").arg(s_current_game_serial));
return;
}
g_emu_thread->reloadGameSettings(false);
}
else
{
// do nothing
return;
}
m_ui.menuCheats->exec(QCursor::pos());
}
void MainWindow::onCheatsMenuAboutToShow()
{
m_ui.menuCheats->clear();
connect(m_ui.menuCheats->addAction(tr("Cheat Manager")), &QAction::triggered, this, &MainWindow::openCheatManager);
connect(m_ui.menuCheats->addAction(tr("Select Cheats...")), &QAction::triggered, this,
[this]() { openGamePropertiesForCurrentGame("Cheats"); });
m_ui.menuCheats->addSeparator();
populateCheatsMenu(m_ui.menuCheats);
}
void MainWindow::populateCheatsMenu(QMenu* menu)
{
const bool has_cheat_list = (s_system_valid && System::HasCheatList());
Host::RunOnCPUThread([menu]() {
if (!System::IsValid())
return;
QMenu* enabled_menu = menu->addMenu(tr("&Enabled Cheats"));
enabled_menu->setEnabled(s_system_valid);
QMenu* apply_menu = menu->addMenu(tr("&Apply Cheats"));
apply_menu->setEnabled(s_system_valid);
if (has_cheat_list)
{
CheatList* cl = System::GetCheatList();
for (const std::string& group : cl->GetCodeGroups())
if (!Cheats::AreCheatsEnabled())
{
QMenu* enabled_submenu = nullptr;
QMenu* apply_submenu = nullptr;
for (u32 i = 0; i < cl->GetCodeCount(); i++)
{
CheatCode& cc = cl->GetCode(i);
if (cc.group != group)
continue;
QString desc(QString::fromStdString(cc.description));
if (cc.IsManuallyActivated())
{
if (!apply_submenu)
{
apply_menu->setEnabled(true);
apply_submenu = apply_menu->addMenu(QString::fromStdString(group));
}
QAction* action = apply_submenu->addAction(desc);
connect(action, &QAction::triggered, [i]() { g_emu_thread->applyCheat(i); });
}
else
{
if (!enabled_submenu)
{
enabled_menu->setEnabled(true);
enabled_submenu = enabled_menu->addMenu(QString::fromStdString(group));
}
QAction* action = enabled_submenu->addAction(desc);
action->setCheckable(true);
action->setChecked(cc.enabled);
connect(action, &QAction::toggled, [i](bool enabled) { g_emu_thread->setCheatEnabled(i, enabled); });
}
}
QAction* action = menu->addAction(tr("Cheats are not enabled."));
action->setEnabled(false);
return;
}
}
QStringList names;
Cheats::EnumerateManualCodes([&names](const std::string& name) {
names.append(QString::fromStdString(name));
return true;
});
if (names.empty())
return;
QtHost::RunOnUIThread([menu, names = std::move(names)]() {
QMenu* apply_submenu = menu->addMenu(tr("&Apply Cheat"));
for (const QString& name : names)
{
const QAction* action = apply_submenu->addAction(name);
connect(action, &QAction::triggered, apply_submenu, [action]() {
Host::RunOnCPUThread([name = action->text().toStdString()]() {
if (System::IsValid())
Cheats::ApplyManualCode(name);
});
});
}
});
});
}
const GameList::Entry* MainWindow::resolveDiscSetEntry(const GameList::Entry* entry,
@ -1375,23 +1304,6 @@ void MainWindow::onViewSystemDisplayTriggered()
switchToEmulationView();
}
void MainWindow::onViewGamePropertiesActionTriggered()
{
if (!s_system_valid)
return;
Host::RunOnCPUThread([]() {
const std::string& path = System::GetDiscPath();
const std::string& serial = System::GetGameSerial();
if (path.empty() || serial.empty())
return;
QtHost::RunOnUIThread([path = path, serial = serial]() {
SettingsWindow::openGamePropertiesDialog(path, System::GetGameTitle(), serial, System::GetDiscRegion());
});
});
}
void MainWindow::onGitHubRepositoryActionTriggered()
{
QtUtils::OpenURL(this, "https://github.com/stenzek/duckstation/");
@ -1486,7 +1398,7 @@ void MainWindow::onGameListEntryContextMenuRequested(const QPoint& point)
if (!entry->IsDiscSet())
{
connect(menu.addAction(tr("Properties...")), &QAction::triggered, [entry]() {
SettingsWindow::openGamePropertiesDialog(entry->path, entry->title, entry->serial, entry->region);
SettingsWindow::openGamePropertiesDialog(entry->path, entry->title, entry->serial, entry->hash, entry->region);
});
connect(menu.addAction(tr("Open Containing Directory...")), &QAction::triggered, [this, entry]() {
@ -1556,7 +1468,7 @@ void MainWindow::onGameListEntryContextMenuRequested(const QPoint& point)
if (first_disc)
{
SettingsWindow::openGamePropertiesDialog(first_disc->path, first_disc->title, first_disc->serial,
first_disc->region);
first_disc->hash, first_disc->region);
}
});
@ -1709,7 +1621,6 @@ void MainWindow::setupAdditionalUi()
m_ui.actionGridViewShowTitles->setChecked(m_game_list_widget->isShowingGridCoverTitles());
updateDebugMenuVisibility();
updateCheatActionsVisibility();
for (u32 i = 0; i < static_cast<u32>(CPUExecutionMode::Count); i++)
{
@ -1795,37 +1706,38 @@ void MainWindow::setupAdditionalUi()
void MainWindow::updateEmulationActions(bool starting, bool running, bool cheevos_challenge_mode)
{
m_ui.actionStartFile->setDisabled(starting || running);
m_ui.actionStartDisc->setDisabled(starting || running);
m_ui.actionStartBios->setDisabled(starting || running);
m_ui.actionResumeLastState->setDisabled(starting || running || cheevos_challenge_mode);
m_ui.actionStartFullscreenUI->setDisabled(starting || running);
m_ui.actionStartFullscreenUI2->setDisabled(starting || running);
const bool starting_or_running = (starting || running);
const bool starting_or_not_running = (starting || !running);
m_ui.actionStartFile->setDisabled(starting_or_running);
m_ui.actionStartDisc->setDisabled(starting_or_running);
m_ui.actionStartBios->setDisabled(starting_or_running);
m_ui.actionResumeLastState->setDisabled(starting_or_running || cheevos_challenge_mode);
m_ui.actionStartFullscreenUI->setDisabled(starting_or_running);
m_ui.actionStartFullscreenUI2->setDisabled(starting_or_running);
m_ui.actionPowerOff->setDisabled(starting || !running);
m_ui.actionPowerOffWithoutSaving->setDisabled(starting || !running);
m_ui.actionReset->setDisabled(starting || !running);
m_ui.actionPause->setDisabled(starting || !running);
m_ui.actionChangeDisc->setDisabled(starting || !running);
m_ui.actionCheats->setDisabled(cheevos_challenge_mode);
m_ui.actionCheatsToolbar->setDisabled(cheevos_challenge_mode);
m_ui.actionScreenshot->setDisabled(starting || !running);
m_ui.menuChangeDisc->setDisabled(starting || !running);
m_ui.menuCheats->setDisabled(cheevos_challenge_mode);
m_ui.actionPowerOff->setDisabled(starting_or_not_running);
m_ui.actionPowerOffWithoutSaving->setDisabled(starting_or_not_running);
m_ui.actionReset->setDisabled(starting_or_not_running);
m_ui.actionPause->setDisabled(starting_or_not_running);
m_ui.actionChangeDisc->setDisabled(starting_or_not_running);
m_ui.actionCheatsToolbar->setDisabled(starting_or_not_running || cheevos_challenge_mode);
m_ui.actionScreenshot->setDisabled(starting_or_not_running);
m_ui.menuChangeDisc->setDisabled(starting_or_not_running);
m_ui.menuCheats->setDisabled(starting_or_not_running || cheevos_challenge_mode);
m_ui.actionCPUDebugger->setDisabled(cheevos_challenge_mode);
m_ui.actionMemoryScanner->setDisabled(cheevos_challenge_mode);
m_ui.actionReloadTextureReplacements->setDisabled(starting || !running);
m_ui.actionDumpRAM->setDisabled(starting || !running || cheevos_challenge_mode);
m_ui.actionDumpVRAM->setDisabled(starting || !running || cheevos_challenge_mode);
m_ui.actionDumpSPURAM->setDisabled(starting || !running || cheevos_challenge_mode);
m_ui.actionReloadTextureReplacements->setDisabled(starting_or_not_running);
m_ui.actionDumpRAM->setDisabled(starting_or_not_running || cheevos_challenge_mode);
m_ui.actionDumpVRAM->setDisabled(starting_or_not_running || cheevos_challenge_mode);
m_ui.actionDumpSPURAM->setDisabled(starting_or_not_running || cheevos_challenge_mode);
m_ui.actionSaveState->setDisabled(starting || !running);
m_ui.menuSaveState->setDisabled(starting || !running);
m_ui.menuWindowSize->setDisabled(starting || !running);
m_ui.actionSaveState->setDisabled(starting_or_not_running);
m_ui.menuSaveState->setDisabled(starting_or_not_running);
m_ui.menuWindowSize->setDisabled(starting_or_not_running);
m_ui.actionViewGameProperties->setDisabled(starting || !running);
m_ui.actionViewGameProperties->setDisabled(starting_or_not_running);
if (starting || running)
if (starting_or_running)
{
if (!m_ui.toolBar->actions().contains(m_ui.actionPowerOff))
{
@ -2037,7 +1949,6 @@ void MainWindow::connectSignals()
connect(m_ui.menuLoadState, &QMenu::aboutToShow, this, &MainWindow::onLoadStateMenuAboutToShow);
connect(m_ui.menuSaveState, &QMenu::aboutToShow, this, &MainWindow::onSaveStateMenuAboutToShow);
connect(m_ui.menuCheats, &QMenu::aboutToShow, this, &MainWindow::onCheatsMenuAboutToShow);
connect(m_ui.actionCheats, &QAction::triggered, this, &MainWindow::onCheatsActionTriggered);
connect(m_ui.actionCheatsToolbar, &QAction::triggered, this, &MainWindow::onCheatsActionTriggered);
connect(m_ui.actionStartFullscreenUI, &QAction::triggered, this, &MainWindow::onStartFullscreenUITriggered);
connect(m_ui.actionStartFullscreenUI2, &QAction::triggered, this, &MainWindow::onStartFullscreenUITriggered);
@ -2081,7 +1992,7 @@ void MainWindow::connectSignals()
connect(m_ui.actionViewGameList, &QAction::triggered, this, &MainWindow::onViewGameListActionTriggered);
connect(m_ui.actionViewGameGrid, &QAction::triggered, this, &MainWindow::onViewGameGridActionTriggered);
connect(m_ui.actionViewSystemDisplay, &QAction::triggered, this, &MainWindow::onViewSystemDisplayTriggered);
connect(m_ui.actionViewGameProperties, &QAction::triggered, this, &MainWindow::onViewGamePropertiesActionTriggered);
connect(m_ui.actionViewGameProperties, &QAction::triggered, this, [this]() { openGamePropertiesForCurrentGame(); });
connect(m_ui.actionGitHubRepository, &QAction::triggered, this, &MainWindow::onGitHubRepositoryActionTriggered);
connect(m_ui.actionDiscordServer, &QAction::triggered, this, &MainWindow::onDiscordServerActionTriggered);
connect(m_ui.actionViewThirdPartyNotices, &QAction::triggered, this,
@ -2095,8 +2006,10 @@ void MainWindow::connectSignals()
connect(m_ui.actionMediaCapture, &QAction::toggled, this, &MainWindow::onToolsMediaCaptureToggled);
connect(m_ui.actionCPUDebugger, &QAction::triggered, this, &MainWindow::openCPUDebugger);
connect(m_ui.actionOpenDataDirectory, &QAction::triggered, this, &MainWindow::onToolsOpenDataDirectoryTriggered);
connect(m_ui.actionOpenTextureDirectory, &QAction::triggered, this, &MainWindow::onToolsOpenTextureDirectoryTriggered);
connect(m_ui.actionReloadTextureReplacements, &QAction::triggered, g_emu_thread, &EmuThread::reloadTextureReplacements);
connect(m_ui.actionOpenTextureDirectory, &QAction::triggered, this,
&MainWindow::onToolsOpenTextureDirectoryTriggered);
connect(m_ui.actionReloadTextureReplacements, &QAction::triggered, g_emu_thread,
&EmuThread::reloadTextureReplacements);
connect(m_ui.actionMergeDiscSets, &QAction::triggered, m_game_list_widget, &GameListWidget::setMergeDiscSets);
connect(m_ui.actionShowGameIcons, &QAction::triggered, m_game_list_widget, &GameListWidget::setShowGameIcons);
connect(m_ui.actionGridViewShowTitles, &QAction::triggered, m_game_list_widget, &GameListWidget::setShowCoverTitles);
@ -2335,6 +2248,25 @@ void MainWindow::doSettings(const char* category /* = nullptr */)
dlg->setCategory(category);
}
void MainWindow::openGamePropertiesForCurrentGame(const char* category /* = nullptr */)
{
if (!s_system_valid)
return;
Host::RunOnCPUThread([category]() {
const std::string& path = System::GetDiscPath();
const std::string& serial = System::GetGameSerial();
if (path.empty() || serial.empty())
return;
QtHost::RunOnUIThread([title = std::string(System::GetGameTitle()), path = std::string(path),
serial = std::string(serial), hash = System::GetGameHash(), region = System::GetDiscRegion(),
category]() {
SettingsWindow::openGamePropertiesDialog(path, title, std::move(serial), hash, region, category);
});
});
}
ControllerSettingsWindow* MainWindow::getControllerSettingsWindow()
{
if (!m_controller_settings_window)
@ -2628,7 +2560,6 @@ void MainWindow::checkForSettingChanges()
{
LogWindow::updateSettings();
updateWindowState();
updateCheatActionsVisibility();
}
std::optional<WindowInfo> MainWindow::getWindowInfo()
@ -2709,7 +2640,6 @@ void MainWindow::onAchievementsChallengeModeChanged(bool enabled)
{
if (enabled)
{
QtUtils::CloseAndDeleteWindow(m_cheat_manager_window);
QtUtils::CloseAndDeleteWindow(m_debugger_window);
QtUtils::CloseAndDeleteWindow(m_memory_scanner_window);
}
@ -2781,23 +2711,6 @@ void MainWindow::onToolsMemoryScannerTriggered()
QtUtils::ShowOrRaiseWindow(m_memory_scanner_window);
}
void MainWindow::openCheatManager()
{
if (Achievements::IsHardcoreModeActive())
return;
if (!m_cheat_manager_window)
{
m_cheat_manager_window = new CheatManagerWindow();
connect(m_cheat_manager_window, &CheatManagerWindow::closed, this, [this]() {
m_cheat_manager_window->deleteLater();
m_cheat_manager_window = nullptr;
});
}
QtUtils::ShowOrRaiseWindow(m_cheat_manager_window);
}
void MainWindow::openCPUDebugger()
{
if (!m_debugger_window)

View File

@ -29,7 +29,6 @@ class GameListWidget;
class EmuThread;
class AutoUpdaterDialog;
class MemoryCardEditorWindow;
class CheatManagerWindow;
class DebuggerWindow;
class MemoryScannerWindow;
@ -96,9 +95,6 @@ public:
ALWAYS_INLINE QLabel* getStatusFPSWidget() const { return m_status_fps_widget; }
ALWAYS_INLINE QLabel* getStatusVPSWidget() const { return m_status_vps_widget; }
/// Accessors for child windows.
CheatManagerWindow* getCheatManagerWindow() const { return m_cheat_manager_window; }
/// Opens the editor for a specific input profile.
void openInputProfileEditor(const std::string_view name);
@ -171,7 +167,6 @@ private Q_SLOTS:
void onViewGameListActionTriggered();
void onViewGameGridActionTriggered();
void onViewSystemDisplayTriggered();
void onViewGamePropertiesActionTriggered();
void onGitHubRepositoryActionTriggered();
void onIssueTrackerActionTriggered();
void onDiscordServerActionTriggered();
@ -193,7 +188,6 @@ private Q_SLOTS:
void onUpdateCheckComplete();
void openCheatManager();
void openCPUDebugger();
protected:
@ -220,7 +214,6 @@ private:
void updateStatusBarWidgetVisibility();
void updateWindowTitle();
void updateWindowState(bool force_visible = false);
void updateCheatActionsVisibility();
void setProgressBar(int current, int total);
void clearProgressBar();
@ -245,6 +238,7 @@ private:
void updateDisplayRelatedActions(bool has_surface, bool render_to_main, bool fullscreen);
void doSettings(const char* category = nullptr);
void openGamePropertiesForCurrentGame(const char* category = nullptr);
void doControllerSettings(ControllerSettingsWindow::Category category = ControllerSettingsWindow::Category::Count);
void updateDebugMenuCPUExecutionMode();
@ -301,7 +295,6 @@ private:
AutoUpdaterDialog* m_auto_updater_dialog = nullptr;
MemoryCardEditorWindow* m_memory_card_editor_window = nullptr;
CheatManagerWindow* m_cheat_manager_window = nullptr;
DebuggerWindow* m_debugger_window = nullptr;
MemoryScannerWindow* m_memory_scanner_window = nullptr;

View File

@ -87,7 +87,6 @@
<addaction name="actionPause"/>
<addaction name="menuChangeDisc"/>
<addaction name="separator"/>
<addaction name="actionCheats"/>
<addaction name="menuCheats"/>
<addaction name="actionScreenshot"/>
<addaction name="separator"/>
@ -481,14 +480,6 @@
<string>Change Disc...</string>
</property>
</action>
<action name="actionCheats">
<property name="icon">
<iconset theme="cheats-line"/>
</property>
<property name="text">
<string>Cheats...</string>
</property>
</action>
<action name="actionCheatsToolbar">
<property name="icon">
<iconset theme="cheats-line"/>

View File

@ -2,7 +2,6 @@
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#include "memoryscannerwindow.h"
#include "cheatcodeeditordialog.h"
#include "qthost.h"
#include "qtutils.h"

View File

@ -5,7 +5,7 @@
#include "ui_memoryscannerwindow.h"
#include "core/cheats.h"
#include "core/memory_scanner.h"
#include <QtCore/QTimer>
#include <QtWidgets/QComboBox>

View File

@ -1292,28 +1292,34 @@ void EmuThread::changeDiscFromPlaylist(quint32 index)
errorReported(tr("Error"), tr("Failed to switch to subimage %1").arg(index));
}
void EmuThread::setCheatEnabled(quint32 index, bool enabled)
void EmuThread::reloadCheats(bool reload_files, bool reload_enabled_list, bool verbose, bool verbose_if_changed)
{
if (!isOnThread())
{
QMetaObject::invokeMethod(this, "setCheatEnabled", Qt::QueuedConnection, Q_ARG(quint32, index),
Q_ARG(bool, enabled));
QMetaObject::invokeMethod(this, "reloadPatches", Qt::QueuedConnection, Q_ARG(bool, reload_files),
Q_ARG(bool, reload_enabled_list), Q_ARG(bool, verbose), Q_ARG(bool, verbose_if_changed));
return;
}
System::SetCheatCodeState(index, enabled);
emit cheatEnabled(index, enabled);
if (System::IsValid())
{
// If the reloaded list is being enabled, we also need to reload the gameini file.
if (reload_enabled_list)
System::ReloadGameSettings(verbose);
Cheats::ReloadCheats(reload_files, reload_enabled_list, verbose, verbose_if_changed);
}
}
void EmuThread::applyCheat(quint32 index)
void EmuThread::applyCheat(const QString& name)
{
if (!isOnThread())
{
QMetaObject::invokeMethod(this, "applyCheat", Qt::QueuedConnection, Q_ARG(quint32, index));
QMetaObject::invokeMethod(this, "applyCheat", Qt::QueuedConnection, Q_ARG(const QString&, name));
return;
}
System::ApplyCheatCode(index);
if (System::IsValid())
Cheats::ApplyManualCode(name.toStdString());
}
void EmuThread::reloadPostProcessingShaders()

View File

@ -158,6 +158,7 @@ public Q_SLOTS:
void setDefaultSettings(bool system = true, bool controller = true);
void applySettings(bool display_osd_messages = false);
void reloadGameSettings(bool display_osd_messages = false);
void reloadCheats(bool reload_files, bool reload_enabled_list, bool verbose, bool verbose_if_changed);
void updateEmuFolders();
void updateControllerSettings();
void reloadInputSources();
@ -194,8 +195,7 @@ public Q_SLOTS:
void setFullscreen(bool fullscreen, bool allow_render_to_main);
void setSurfaceless(bool surfaceless);
void requestDisplaySize(float scale);
void setCheatEnabled(quint32 index, bool enabled);
void applyCheat(quint32 index);
void applyCheat(const QString& name);
void reloadPostProcessingShaders();
void updatePostProcessingSettings();
void clearInputBindStateFromSource(InputBindingKey key);

View File

@ -31,6 +31,7 @@
<file>icons/black/svg/arrow-left-right-line.svg</file>
<file>icons/black/svg/arrow-up-line.svg</file>
<file>icons/black/svg/artboard-2-line.svg</file>
<file>icons/black/svg/chat-off-line.svg</file>
<file>icons/black/svg/cheats-line.svg</file>
<file>icons/black/svg/checkbox-multiple-blank-line.svg</file>
<file>icons/black/svg/chip-2-line.svg</file>
@ -55,6 +56,7 @@
<file>icons/black/svg/download-2-line.svg</file>
<file>icons/black/svg/eject-line.svg</file>
<file>icons/black/svg/emulation-line.svg</file>
<file>icons/black/svg/export-line.svg</file>
<file>icons/black/svg/file-add-line.svg</file>
<file>icons/black/svg/file-line.svg</file>
<file>icons/black/svg/file-list-line.svg</file>
@ -74,6 +76,7 @@
<file>icons/black/svg/global-line.svg</file>
<file>icons/black/svg/guncon-line.svg</file>
<file>icons/black/svg/image-fill.svg</file>
<file>icons/black/svg/import-line.svg</file>
<file>icons/black/svg/information-line.svg</file>
<file>icons/black/svg/joystick-line.svg</file>
<file>icons/black/svg/keyboard-line.svg</file>
@ -98,6 +101,7 @@
<file>icons/black/svg/settings-3-line.svg</file>
<file>icons/black/svg/shut-down-line.svg</file>
<file>icons/black/svg/sparkle-fill.svg</file>
<file>icons/black/svg/sparkling-line.svg</file>
<file>icons/black/svg/sun-fill.svg</file>
<file>icons/black/svg/trash-fill.svg</file>
<file>icons/black/svg/trophy-line.svg</file>
@ -249,6 +253,7 @@
<file>icons/white/svg/artboard-2-line.svg</file>
<file>icons/white/svg/cheats-line.svg</file>
<file>icons/white/svg/checkbox-multiple-blank-line.svg</file>
<file>icons/white/svg/chat-off-line.svg</file>
<file>icons/white/svg/chip-2-line.svg</file>
<file>icons/white/svg/chip-line.svg</file>
<file>icons/white/svg/close-line.svg</file>
@ -271,6 +276,7 @@
<file>icons/white/svg/download-2-line.svg</file>
<file>icons/white/svg/eject-line.svg</file>
<file>icons/white/svg/emulation-line.svg</file>
<file>icons/white/svg/export-line.svg</file>
<file>icons/white/svg/file-add-line.svg</file>
<file>icons/white/svg/file-line.svg</file>
<file>icons/white/svg/file-list-line.svg</file>
@ -290,6 +296,7 @@
<file>icons/white/svg/global-line.svg</file>
<file>icons/white/svg/guncon-line.svg</file>
<file>icons/white/svg/image-fill.svg</file>
<file>icons/white/svg/import-line.svg</file>
<file>icons/white/svg/information-line.svg</file>
<file>icons/white/svg/joystick-line.svg</file>
<file>icons/white/svg/keyboard-line.svg</file>
@ -314,6 +321,7 @@
<file>icons/white/svg/settings-3-line.svg</file>
<file>icons/white/svg/shut-down-line.svg</file>
<file>icons/white/svg/sparkle-fill.svg</file>
<file>icons/white/svg/sparkling-line.svg</file>
<file>icons/white/svg/sun-fill.svg</file>
<file>icons/white/svg/trash-fill.svg</file>
<file>icons/white/svg/trophy-line.svg</file>

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000">
<path d="M2.80777 1.3934L22.6068 21.1924L21.1925 22.6066L17.5846 18.9994L6.45516 19L2.00016 22.5V4C2.00016 3.8307 2.04223 3.67123 2.11649 3.53146L1.39355 2.80762L2.80777 1.3934ZM3.99955 5.4134L4.00016 18.3853L5.76349 17L15.5846 16.9994L3.99955 5.4134ZM21.0002 3C21.5524 3 22.0002 3.44772 22.0002 4V17.785L20.0002 15.785V5L9.21316 4.999L7.21416 3H21.0002Z"></path>
</svg>

After

Width:  |  Height:  |  Size: 447 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000"><path d="M22 4C22 3.44772 21.5523 3 21 3H3C2.44772 3 2 3.44772 2 4V20C2 20.5523 2.44772 21 3 21H21C21.5523 21 22 20.5523 22 20V4ZM4 15H7.41604C8.1876 16.7659 9.94968 18 12 18C14.0503 18 15.8124 16.7659 16.584 15H20V19H4V15ZM4 5H20V13H15C15 14.6569 13.6569 16 12 16C10.3431 16 9 14.6569 9 13H4V5ZM16 11H13V14H11V11H8L12 6.5L16 11Z"></path></svg>

After

Width:  |  Height:  |  Size: 419 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000"><path d="M22 4C22 3.44772 21.5523 3 21 3H3C2.44772 3 2 3.44772 2 4V20C2 20.5523 2.44772 21 3 21H21C21.5523 21 22 20.5523 22 20V4ZM4 15H7.41604C8.1876 16.7659 9.94968 18 12 18C14.0503 18 15.8124 16.7659 16.584 15H20V19H4V15ZM4 5H20V13H15C15 14.6569 13.6569 16 12 16C10.3431 16 9 14.6569 9 13H4V5ZM16 9H13V6H11V9H8L12 13.5L16 9Z"></path></svg>

After

Width:  |  Height:  |  Size: 416 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000">
<path d="M14 4.4375C15.3462 4.4375 16.4375 3.34619 16.4375 2H17.5625C17.5625 3.34619 18.6538 4.4375 20 4.4375V5.5625C18.6538 5.5625 17.5625 6.65381 17.5625 8H16.4375C16.4375 6.65381 15.3462 5.5625 14 5.5625V4.4375ZM1 11C4.31371 11 7 8.31371 7 5H9C9 8.31371 11.6863 11 15 11V13C11.6863 13 9 15.6863 9 19H7C7 15.6863 4.31371 13 1 13V11ZM4.87601 12C6.18717 12.7276 7.27243 13.8128 8 15.124 8.72757 13.8128 9.81283 12.7276 11.124 12 9.81283 11.2724 8.72757 10.1872 8 8.87601 7.27243 10.1872 6.18717 11.2724 4.87601 12ZM17.25 14C17.25 15.7949 15.7949 17.25 14 17.25V18.75C15.7949 18.75 17.25 20.2051 17.25 22H18.75C18.75 20.2051 20.2051 18.75 22 18.75V17.25C20.2051 17.25 18.75 15.7949 18.75 14H17.25Z"></path>
</svg>

After

Width:  |  Height:  |  Size: 789 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff">
<path d="M2.80777 1.3934L22.6068 21.1924L21.1925 22.6066L17.5846 18.9994L6.45516 19L2.00016 22.5V4C2.00016 3.8307 2.04223 3.67123 2.11649 3.53146L1.39355 2.80762L2.80777 1.3934ZM3.99955 5.4134L4.00016 18.3853L5.76349 17L15.5846 16.9994L3.99955 5.4134ZM21.0002 3C21.5524 3 22.0002 3.44772 22.0002 4V17.785L20.0002 15.785V5L9.21316 4.999L7.21416 3H21.0002Z"></path>
</svg>

After

Width:  |  Height:  |  Size: 447 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M22 4C22 3.44772 21.5523 3 21 3H3C2.44772 3 2 3.44772 2 4V20C2 20.5523 2.44772 21 3 21H21C21.5523 21 22 20.5523 22 20V4ZM4 15H7.41604C8.1876 16.7659 9.94968 18 12 18C14.0503 18 15.8124 16.7659 16.584 15H20V19H4V15ZM4 5H20V13H15C15 14.6569 13.6569 16 12 16C10.3431 16 9 14.6569 9 13H4V5ZM16 11H13V14H11V11H8L12 6.5L16 11Z"></path></svg>

After

Width:  |  Height:  |  Size: 419 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M22 4C22 3.44772 21.5523 3 21 3H3C2.44772 3 2 3.44772 2 4V20C2 20.5523 2.44772 21 3 21H21C21.5523 21 22 20.5523 22 20V4ZM4 15H7.41604C8.1876 16.7659 9.94968 18 12 18C14.0503 18 15.8124 16.7659 16.584 15H20V19H4V15ZM4 5H20V13H15C15 14.6569 13.6569 16 12 16C10.3431 16 9 14.6569 9 13H4V5ZM16 9H13V6H11V9H8L12 13.5L16 9Z"></path></svg>

After

Width:  |  Height:  |  Size: 416 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff">
<path d="M14 4.4375C15.3462 4.4375 16.4375 3.34619 16.4375 2H17.5625C17.5625 3.34619 18.6538 4.4375 20 4.4375V5.5625C18.6538 5.5625 17.5625 6.65381 17.5625 8H16.4375C16.4375 6.65381 15.3462 5.5625 14 5.5625V4.4375ZM1 11C4.31371 11 7 8.31371 7 5H9C9 8.31371 11.6863 11 15 11V13C11.6863 13 9 15.6863 9 19H7C7 15.6863 4.31371 13 1 13V11ZM4.87601 12C6.18717 12.7276 7.27243 13.8128 8 15.124 8.72757 13.8128 9.81283 12.7276 11.124 12 9.81283 11.2724 8.72757 10.1872 8 8.87601 7.27243 10.1872 6.18717 11.2724 4.87601 12ZM17.25 14C17.25 15.7949 15.7949 17.25 14 17.25V18.75C15.7949 18.75 17.25 20.2051 17.25 22H18.75C18.75 20.2051 20.2051 18.75 22 18.75V17.25C20.2051 17.25 18.75 15.7949 18.75 14H17.25Z"></path>
</svg>

After

Width:  |  Height:  |  Size: 789 B

View File

@ -9,7 +9,9 @@
#include "consolesettingswidget.h"
#include "emulationsettingswidget.h"
#include "foldersettingswidget.h"
#include "gamecheatsettingswidget.h"
#include "gamelistsettingswidget.h"
#include "gamepatchsettingswidget.h"
#include "gamesummarywidget.h"
#include "graphicssettingswidget.h"
#include "interfacesettingswidget.h"
@ -46,9 +48,9 @@ SettingsWindow::SettingsWindow() : QWidget()
connectUi();
}
SettingsWindow::SettingsWindow(const std::string& path, const std::string& serial, DiscRegion region,
SettingsWindow::SettingsWindow(const std::string& path, const std::string& serial, GameHash hash, DiscRegion region,
const GameDatabase::Entry* entry, std::unique_ptr<INISettingsInterface> sif)
: QWidget(), m_sif(std::move(sif)), m_database_entry(entry)
: QWidget(), m_sif(std::move(sif)), m_database_entry(entry), m_serial(serial), m_hash(hash)
{
m_ui.setupUi(this);
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
@ -108,6 +110,23 @@ void SettingsWindow::addPages()
QStringLiteral("emulation-line"),
tr("<strong>Emulation Settings</strong><hr>These options determine the speed and runahead behavior of the "
"system.<br><br>Mouse over an option for additional information, and Shift+Wheel to scroll this panel."));
if (isPerGameSettings())
{
addWidget(m_game_patch_settings_widget = new GamePatchSettingsWidget(this, m_ui.settingsContainer), tr("Patches"),
QStringLiteral("sparkling-line"),
tr("<strong>Patches</strong><hr>This section allows you to select optional patches to apply to the game, "
"which may provide performance, visual, or gameplay improvements. Activating game patches can cause "
"unpredictable behavior, crashing, soft-locks, or broken saved games. Use patches at your own risk, "
"no support will be provided to users who have enabled game patches."));
addWidget(m_game_cheat_settings_widget = new GameCheatSettingsWidget(this, m_ui.settingsContainer), tr("Cheats"),
QStringLiteral("cheats-line"),
tr("<strong>Cheats</strong><hr>This section allows you to select which cheats you wish to enable. "
"<strong>Using cheats can have unpredictable effects on games, causing crashes, graphical glitches, "
"and corrupted saves.</strong> Cheats also persist through save states even after being disabled, "
"please remember to reset/reboot the game after turning off any codes."));
}
addWidget(
m_memory_card_settings = new MemoryCardSettingsWidget(this, m_ui.settingsContainer), tr("Memory Cards"),
QStringLiteral("memcard-line"),
@ -635,8 +654,9 @@ bool SettingsWindow::hasGameTrait(GameDatabase::Trait trait)
m_sif->GetBoolValue("Main", "ApplyCompatibilitySettings", true));
}
void SettingsWindow::openGamePropertiesDialog(const std::string& path, const std::string& title,
const std::string& serial, DiscRegion region)
SettingsWindow* SettingsWindow::openGamePropertiesDialog(const std::string& path, const std::string& title,
std::string serial, GameHash hash, DiscRegion region,
const char* category /* = nullptr */)
{
const GameDatabase::Entry* dentry = nullptr;
if (!System::IsExeFileName(path) && !System::IsPsfFileName(path))
@ -669,7 +689,9 @@ void SettingsWindow::openGamePropertiesDialog(const std::string& path, const std
dialog->raise();
dialog->activateWindow();
dialog->setFocus();
return;
if (category)
dialog->setCategory(category);
return dialog;
}
}
@ -677,8 +699,11 @@ void SettingsWindow::openGamePropertiesDialog(const std::string& path, const std
if (FileSystem::FileExists(sif->GetFileName().c_str()))
sif->Load();
SettingsWindow* dialog = new SettingsWindow(path, real_serial, region, dentry, std::move(sif));
SettingsWindow* dialog = new SettingsWindow(path, real_serial, hash, region, dentry, std::move(sif));
dialog->show();
if (category)
dialog->setCategory(category);
return dialog;
}
void SettingsWindow::closeGamePropertiesDialogs()

View File

@ -6,12 +6,13 @@
#include "util/ini_settings_interface.h"
#include "common/types.h"
#include "core/types.h"
#include <QtCore/QMap>
#include <QtCore/QString>
#include <QtWidgets/QDialog>
#include <array>
#include <optional>
class QWheelEvent;
@ -25,6 +26,8 @@ struct Entry;
class InterfaceSettingsWidget;
class BIOSSettingsWidget;
class GameListSettingsWidget;
class GamePatchSettingsWidget;
class GameCheatSettingsWidget;
class ConsoleSettingsWidget;
class EmulationSettingsWidget;
class MemoryCardSettingsWidget;
@ -41,12 +44,12 @@ class SettingsWindow final : public QWidget
public:
SettingsWindow();
SettingsWindow(const std::string& path, const std::string& serial, DiscRegion region,
SettingsWindow(const std::string& path, const std::string& serial, GameHash hash, DiscRegion region,
const GameDatabase::Entry* entry, std::unique_ptr<INISettingsInterface> sif);
~SettingsWindow();
static void openGamePropertiesDialog(const std::string& path, const std::string& title, const std::string& serial,
DiscRegion region);
static SettingsWindow* openGamePropertiesDialog(const std::string& path, const std::string& title, std::string serial,
GameHash hash, DiscRegion region, const char* category = nullptr);
static void closeGamePropertiesDialogs();
// Helper for externally setting fields in game settings ini.
@ -56,6 +59,8 @@ public:
ALWAYS_INLINE bool isPerGameSettings() const { return static_cast<bool>(m_sif); }
ALWAYS_INLINE INISettingsInterface* getSettingsInterface() const { return m_sif.get(); }
ALWAYS_INLINE const std::string& getGameSerial() const { return m_serial; }
ALWAYS_INLINE const std::optional<GameHash>& getGameHash() const { return m_hash; }
ALWAYS_INLINE InterfaceSettingsWidget* getInterfaceSettingsWidget() const { return m_interface_settings; }
ALWAYS_INLINE BIOSSettingsWidget* getBIOSSettingsWidget() const { return m_bios_settings; }
@ -93,7 +98,6 @@ public:
bool containsSettingValue(const char* section, const char* key) const;
void removeSettingValue(const char* section, const char* key);
void saveAndReloadGameSettings();
void reloadGameSettingsFromIni();
bool hasGameTrait(GameDatabase::Trait trait);
@ -117,7 +121,7 @@ protected:
private:
enum : u32
{
MAX_SETTINGS_WIDGETS = 12
MAX_SETTINGS_WIDGETS = 13
};
void connectUi();
@ -137,6 +141,8 @@ private:
ConsoleSettingsWidget* m_console_settings = nullptr;
EmulationSettingsWidget* m_emulation_settings = nullptr;
GameListSettingsWidget* m_game_list_settings = nullptr;
GamePatchSettingsWidget* m_game_patch_settings_widget = nullptr;
GameCheatSettingsWidget* m_game_cheat_settings_widget = nullptr;
MemoryCardSettingsWidget* m_memory_card_settings = nullptr;
GraphicsSettingsWidget* m_graphics_settings = nullptr;
PostProcessingSettingsWidget* m_post_processing_settings = nullptr;
@ -150,5 +156,6 @@ private:
QObject* m_current_help_widget = nullptr;
QMap<QObject*, QString> m_widget_help_text_map;
std::string m_game_list_filename;
std::string m_serial;
std::optional<GameHash> m_hash;
};

View File

@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
static constexpr ImWchar FA_ICON_RANGE[] = { 0xe06f,0xe06f,0xe086,0xe086,0xf002,0xf002,0xf005,0xf005,0xf007,0xf007,0xf00c,0xf00e,0xf011,0xf011,0xf013,0xf013,0xf017,0xf017,0xf019,0xf019,0xf01c,0xf01c,0xf021,0xf021,0xf023,0xf023,0xf025,0xf025,0xf02e,0xf02e,0xf030,0xf030,0xf03a,0xf03a,0xf03d,0xf03d,0xf04a,0xf04c,0xf050,0xf050,0xf05e,0xf05e,0xf062,0xf063,0xf067,0xf067,0xf071,0xf071,0xf075,0xf075,0xf077,0xf078,0xf07b,0xf07c,0xf084,0xf085,0xf091,0xf091,0xf0a0,0xf0a0,0xf0ac,0xf0ad,0xf0c5,0xf0c5,0xf0c7,0xf0c9,0xf0cb,0xf0cb,0xf0d0,0xf0d0,0xf0dc,0xf0dc,0xf0e2,0xf0e2,0xf0e7,0xf0e7,0xf0eb,0xf0eb,0xf0f1,0xf0f1,0xf0f3,0xf0f3,0xf0fe,0xf0fe,0xf110,0xf110,0xf119,0xf119,0xf11b,0xf11c,0xf140,0xf140,0xf14a,0xf14a,0xf15b,0xf15b,0xf15d,0xf15d,0xf191,0xf192,0xf1ab,0xf1ab,0xf1dd,0xf1de,0xf1e6,0xf1e6,0xf1eb,0xf1eb,0xf1f8,0xf1f8,0xf1fc,0xf1fc,0xf240,0xf240,0xf242,0xf242,0xf245,0xf245,0xf26c,0xf26c,0xf279,0xf279,0xf2d0,0xf2d0,0xf2db,0xf2db,0xf2f2,0xf2f2,0xf3fd,0xf3fd,0xf410,0xf410,0xf466,0xf466,0xf4ce,0xf4ce,0xf500,0xf500,0xf51f,0xf51f,0xf538,0xf538,0xf545,0xf545,0xf547,0xf548,0xf57a,0xf57a,0xf5a2,0xf5a2,0xf5aa,0xf5aa,0xf5e7,0xf5e7,0xf65d,0xf65e,0xf6cf,0xf6cf,0xf70c,0xf70c,0xf794,0xf794,0xf7a0,0xf7a0,0xf7c2,0xf7c2,0xf807,0xf807,0xf815,0xf815,0xf818,0xf818,0xf84c,0xf84c,0xf8cc,0xf8cc,0x0,0x0 };
static constexpr ImWchar FA_ICON_RANGE[] = { 0xe06f,0xe06f,0xe086,0xe086,0xf002,0xf002,0xf005,0xf005,0xf007,0xf007,0xf00c,0xf00e,0xf011,0xf011,0xf013,0xf013,0xf017,0xf017,0xf019,0xf019,0xf01c,0xf01c,0xf021,0xf021,0xf023,0xf023,0xf025,0xf025,0xf02e,0xf02e,0xf030,0xf030,0xf03a,0xf03a,0xf03d,0xf03d,0xf04a,0xf04c,0xf050,0xf050,0xf05e,0xf05e,0xf062,0xf063,0xf067,0xf067,0xf071,0xf071,0xf075,0xf075,0xf077,0xf078,0xf07b,0xf07c,0xf084,0xf085,0xf091,0xf091,0xf0ac,0xf0ad,0xf0c3,0xf0c3,0xf0c5,0xf0c5,0xf0c7,0xf0c9,0xf0cb,0xf0cb,0xf0d0,0xf0d0,0xf0dc,0xf0dc,0xf0e2,0xf0e2,0xf0e7,0xf0e7,0xf0eb,0xf0eb,0xf0f1,0xf0f1,0xf0f3,0xf0f3,0xf0fe,0xf0fe,0xf110,0xf110,0xf119,0xf119,0xf11b,0xf11c,0xf140,0xf140,0xf14a,0xf14a,0xf15b,0xf15b,0xf15d,0xf15d,0xf191,0xf192,0xf1ab,0xf1ab,0xf1dd,0xf1de,0xf1e6,0xf1e6,0xf1eb,0xf1eb,0xf1f8,0xf1f8,0xf1fc,0xf1fc,0xf240,0xf240,0xf242,0xf242,0xf245,0xf245,0xf26c,0xf26c,0xf279,0xf279,0xf2d0,0xf2d0,0xf2db,0xf2db,0xf2f2,0xf2f2,0xf3fd,0xf3fd,0xf410,0xf410,0xf462,0xf462,0xf466,0xf466,0xf4ce,0xf4ce,0xf500,0xf500,0xf51f,0xf51f,0xf538,0xf538,0xf545,0xf545,0xf547,0xf548,0xf57a,0xf57a,0xf5a2,0xf5a2,0xf5aa,0xf5aa,0xf5e7,0xf5e7,0xf65d,0xf65e,0xf6cf,0xf6cf,0xf70c,0xf70c,0xf794,0xf794,0xf7a0,0xf7a0,0xf7c2,0xf7c2,0xf807,0xf807,0xf815,0xf815,0xf818,0xf818,0xf84c,0xf84c,0xf8cc,0xf8cc,0x0,0x0 };
static constexpr ImWchar PF_ICON_RANGE[] = { 0x2196,0x2199,0x219e,0x21a1,0x21b0,0x21b3,0x21ba,0x21c3,0x21c7,0x21ca,0x21d0,0x21d4,0x21dc,0x21dd,0x21e0,0x21e3,0x21ed,0x21ee,0x21f7,0x21f8,0x21fa,0x21fb,0x227a,0x227f,0x2284,0x2284,0x2349,0x2349,0x235e,0x235e,0x2360,0x2361,0x2364,0x2366,0x23b2,0x23b4,0x23ce,0x23ce,0x23f4,0x23f7,0x2427,0x243a,0x243c,0x243e,0x2460,0x246b,0x248f,0x248f,0x24f5,0x24fd,0x24ff,0x24ff,0x2717,0x2717,0x278a,0x278e,0x27fc,0x27fc,0xe001,0xe001,0xff21,0xff3a,0x1f52b,0x1f52b,0x0,0x0 };