diff --git a/src/core-tests/CMakeLists.txt b/src/core-tests/CMakeLists.txt index 13ca690c7..1db5ba832 100644 --- a/src/core-tests/CMakeLists.txt +++ b/src/core-tests/CMakeLists.txt @@ -2,11 +2,22 @@ # SPDX-License-Identifier: CC-BY-NC-ND-4.0 + Packaging Restriction add_executable(core-tests + ../core/cheats.cpp ../core/cpu_disasm.cpp ../core/cpu_types.cpp + ../util/zip_helpers.cpp + cheats_tests.cpp cpu_disasm_tests.cpp stub_cpu.cpp ) target_include_directories(core-tests PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/..") -target_link_libraries(core-tests PRIVATE common gtest gtest_main) +target_link_libraries(core-tests PRIVATE common imgui libzip::zip gtest gtest_main) + +if(APPLE) + target_compile_options(core-tests PRIVATE -ffunction-sections -fdata-sections) + target_link_options(core-tests PRIVATE -Wl,-dead_strip) +elseif(NOT MSVC) + target_compile_options(core-tests PRIVATE -ffunction-sections -fdata-sections) + target_link_options(core-tests PRIVATE -Wl,--gc-sections) +endif() diff --git a/src/core-tests/cheats_tests.cpp b/src/core-tests/cheats_tests.cpp new file mode 100644 index 000000000..1bc981435 --- /dev/null +++ b/src/core-tests/cheats_tests.cpp @@ -0,0 +1,292 @@ +// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin +// SPDX-License-Identifier: CC-BY-NC-ND-4.0 + +#include "core/cheats_private.h" + +#include "common/error.h" + +#include +#include + +namespace { + +static Cheats::CheatCode::Metadata MakeMetadata(Cheats::CodeType type, + Cheats::CodeActivation activation = Cheats::CodeActivation::EndFrame) +{ + Cheats::CheatCode::Metadata metadata = {}; + metadata.name = "Test Code"; + metadata.type = type; + metadata.activation = activation; + return metadata; +} + +static void ExpectAssemblyInstructions(const Cheats::AssemblyCheatCode& code, + std::span> expected) +{ + ASSERT_EQ(code.GetInstructions().size(), expected.size()); + for (size_t i = 0; i < expected.size(); i++) + { + EXPECT_EQ(code.GetInstructions()[i].pc, expected[i].first) << i; + EXPECT_EQ(code.GetInstructions()[i].new_value, expected[i].second) << i; + EXPECT_EQ(code.GetInstructions()[i].old_value, Cheats::AssemblyCheatCode::UNINITIALIZED_OLD_VALUE) << i; + } +} + +} // namespace + +TEST(Cheats, ParseCodeInfo) +{ + static constexpr std::string_view input = "# ignored comment\n" + "[Group\\Code]\n" + "Author = Test Author\n" + "Description = Test Description\n" + "Type = Assembly\n" + "Activation = EndFrame\n" + "Option = Easy:0x12\n" + "Option = Hard:52\n" + "OptionRange = 1:255\n" + "DisallowForAchievements = true\n" + ".org 0x80010000\n" + "addiu t0, zero, 1\n"; + + Cheats::CodeInfoList codes; + Error error; + ASSERT_TRUE(Cheats::ImportCodesFromString(&codes, input, Cheats::FileFormat::DuckStation, true, &error)) + << error.GetDescription(); + ASSERT_EQ(codes.size(), 1u); + + const Cheats::CodeInfo& code = codes.front(); + EXPECT_EQ(code.name, "Group\\Code"); + EXPECT_EQ(code.GetNameParentPart(), "Group"); + EXPECT_EQ(code.GetNamePart(), "Code"); + EXPECT_EQ(code.author, "Test Author"); + EXPECT_EQ(code.description, "Test Description"); + EXPECT_EQ(code.type, Cheats::CodeType::Assembly); + EXPECT_EQ(code.activation, Cheats::CodeActivation::EndFrame); + EXPECT_FALSE(code.from_database); + EXPECT_TRUE(code.disallow_for_achievements); + ASSERT_EQ(code.options.size(), 2u); + EXPECT_EQ(code.options[0], Cheats::CodeOption("Easy", 0x12)); + EXPECT_EQ(code.options[1], Cheats::CodeOption("Hard", 52)); + EXPECT_EQ(code.option_range_start, 1); + EXPECT_EQ(code.option_range_end, 255); + EXPECT_EQ(code.body, ".org 0x80010000\naddiu t0, zero, 1"); + EXPECT_EQ(input.substr(code.file_offset_start, code.file_offset_body_start - code.file_offset_start), + "[Group\\Code]\nAuthor = Test Author\nDescription = Test Description\nType = Assembly\n" + "Activation = EndFrame\nOption = Easy:0x12\nOption = Hard:52\nOptionRange = 1:255\n" + "DisallowForAchievements = true\n"); + EXPECT_EQ(input.substr(code.file_offset_body_start, code.file_offset_end - code.file_offset_body_start), + ".org 0x80010000\naddiu t0, zero, 1\n"); +} + +TEST(Cheats, ParseCodeInfoLegacyIgnoreAndReplacement) +{ + static constexpr std::string_view input = "#group=Legacy\n" + "#type=Gameshark\n" + "#activation=Manual\n" + "[Ignored]\n" + "Ignore = true\n" + "80010000 0001\n" + "[Duplicate]\n" + "80010000 0002\n" + "[Duplicate]\n" + "80010000 0003\n"; + + Cheats::CodeInfoList codes; + Error error; + ASSERT_TRUE(Cheats::ImportCodesFromString(&codes, input, Cheats::FileFormat::DuckStation, true, &error)) + << error.GetDescription(); + ASSERT_EQ(codes.size(), 1u); + EXPECT_EQ(codes[0].name, "Legacy\\Duplicate"); + EXPECT_EQ(codes[0].type, Cheats::CodeType::Gameshark); + EXPECT_EQ(codes[0].activation, Cheats::CodeActivation::Manual); + EXPECT_EQ(codes[0].body, "80010000 0003"); +} + +TEST(Cheats, ParseCodeInfoErrors) +{ + Cheats::CodeInfoList codes; + Error error; + EXPECT_FALSE(Cheats::ImportCodesFromString(&codes, "[Code]\nOptionRange = 10:1\n80010000 0001\n", + Cheats::FileFormat::DuckStation, true, &error)); + EXPECT_TRUE(error.IsValid()); +} + +TEST(Cheats, ParseGamesharkCode) +{ + static constexpr std::string_view input = "30000010 00000012\n" // ConstantWrite8 + "80000020 00003456\n" // ConstantWrite16 + "90000030 89ABCDEF\n" // ExtConstantWrite32 + "10000040 00000001\n" // Increment16 + "11000050 00000002\n" // Decrement16 + "20000060 00000003\n" // Increment8 + "21000070 00000004\n" // Decrement8 + "60000080 00000005\n" // ExtIncrement32 + "61000090 00000006\n" // ExtDecrement32 + "D00000A0 00001111\n" // CompareEqual16 + "D10000B0 00002222\n" // CompareNotEqual16 + "D20000C0 00003333\n" // CompareLess16 + "D30000D0 00004444\n" // CompareGreater16 + "E00000E0 00000055\n" // CompareEqual8 + "E10000F0 00000066\n" // CompareNotEqual8 + "A0000100 12345678\n" // ExtCompareEqual32 + "A1010110 87654321\n" // ExtCompareNotEqual32 + "50000203 00040005\n" // Slide + "C2000120 00000130\n" // MemoryCopy + "53000140 01020304\n" // ExtImprovedSlide + "31000150 00000080\n" // ExtConstantBitSet8 + "82000160 00004000\n" // ExtConstantBitClear16 + "91000170 DEADBEEF\n" // ExtConstantBitSet32 + "F4000180 00000190\n" // ExtFindAndReplace + "51000001 00000002\n" // ExtCheatRegisters + "52000003 00000004\n"; // ExtCheatRegistersCompare + static constexpr std::array expected = { + std::pair{0x30000010u, 0x00000012u}, std::pair{0x80000020u, 0x00003456u}, std::pair{0x90000030u, 0x89ABCDEFu}, + std::pair{0x10000040u, 0x00000001u}, std::pair{0x11000050u, 0x00000002u}, std::pair{0x20000060u, 0x00000003u}, + std::pair{0x21000070u, 0x00000004u}, std::pair{0x60000080u, 0x00000005u}, std::pair{0x61000090u, 0x00000006u}, + std::pair{0xD00000A0u, 0x00001111u}, std::pair{0xD10000B0u, 0x00002222u}, std::pair{0xD20000C0u, 0x00003333u}, + std::pair{0xD30000D0u, 0x00004444u}, std::pair{0xE00000E0u, 0x00000055u}, std::pair{0xE10000F0u, 0x00000066u}, + std::pair{0xA0000100u, 0x12345678u}, std::pair{0xA1010110u, 0x87654321u}, std::pair{0x50000203u, 0x00040005u}, + std::pair{0xC2000120u, 0x00000130u}, std::pair{0x53000140u, 0x01020304u}, std::pair{0x31000150u, 0x00000080u}, + std::pair{0x82000160u, 0x00004000u}, std::pair{0x91000170u, 0xDEADBEEFu}, std::pair{0xF4000180u, 0x00000190u}, + std::pair{0x51000001u, 0x00000002u}, std::pair{0x52000003u, 0x00000004u}, + }; + + Error error; + std::unique_ptr base = Cheats::ParseCode(MakeMetadata(Cheats::CodeType::Gameshark), input, &error); + ASSERT_NE(base, nullptr) << error.GetDescription(); + Cheats::GamesharkCheatCode* code = static_cast(base.get()); + ASSERT_EQ(code->GetInstructions().size(), expected.size()); + for (size_t i = 0; i < expected.size(); i++) + { + EXPECT_EQ(code->GetInstructions()[i].first, expected[i].first) << i; + EXPECT_EQ(code->GetInstructions()[i].second, expected[i].second) << i; + } +} + +TEST(Cheats, ParseGamesharkOptions) +{ + Cheats::CheatCode::Metadata metadata = MakeMetadata(Cheats::CodeType::Gameshark); + metadata.has_options = true; + Error error; + std::unique_ptr base = + Cheats::ParseCode(std::move(metadata), "30000010 000000??\n80000020 00??0000\n90000030 ????????\n", &error); + ASSERT_NE(base, nullptr) << error.GetDescription(); + Cheats::GamesharkCheatCode* code = static_cast(base.get()); + + code->SetOptionValue(0x89ABCDEF); + ASSERT_EQ(code->GetInstructions().size(), 3u); + EXPECT_EQ(code->GetInstructions()[0].second, 0x000000EFu); + EXPECT_EQ(code->GetInstructions()[1].second, 0x00EF0000u); + EXPECT_EQ(code->GetInstructions()[2].second, 0x89ABCDEFu); +} + +TEST(Cheats, ParseGamesharkCodeErrors) +{ + static constexpr std::array cases = { + "", + "80010000", + "80010000 zzzzzzzz", + "80010000 00??00??", + }; + for (const std::string_view input : cases) + { + Error error; + EXPECT_EQ(Cheats::ParseCode(MakeMetadata(Cheats::CodeType::Gameshark), input, &error), nullptr) << input; + EXPECT_TRUE(error.IsValid()) << input; + } +} + +TEST(Cheats, ParseAssemblyCode) +{ + Cheats::CheatCode::Metadata metadata = MakeMetadata(Cheats::CodeType::Assembly); + metadata.has_options = true; + static constexpr std::string_view input = ".org 0x80010000\n" + "start: addiu t0, zero, 1\n" + "beq t0, zero, target\n" + "j start\n" + "nop\n" + "target: ori t1, zero, 0x????\n"; + + Error error; + std::unique_ptr base = Cheats::ParseCode(std::move(metadata), input, &error); + ASSERT_NE(base, nullptr) << error.GetDescription(); + Cheats::AssemblyCheatCode* code = static_cast(base.get()); + static constexpr std::array expected = { + std::pair{0x80010000u, 0x24080001u}, std::pair{0x80010004u, 0x11000002u}, std::pair{0x80010008u, 0x08004000u}, + std::pair{0x8001000Cu, 0x00000000u}, std::pair{0x80010010u, 0x34090000u}, + }; + ExpectAssemblyInstructions(*code, expected); + + code->SetOptionValue(0x1234); + EXPECT_EQ(code->GetInstructions().back().new_value, 0x34091234u); +} + +TEST(Cheats, ParseAssemblyInstructionFormatsAndOrigins) +{ + static constexpr std::string_view input = "; first section\n" + ".org 0x80020000\n" + "lui t0, 0x1234\n" + "ori t0, t0, 0x5678 # construct constant\n" + "sw t0, 16(sp)\n" + "lw v0, -4(sp)\n" + "addu v1, v0, t0\n" + ".org 0x80030000\n" + "jal 0x80031000\n" + "jr ra\n"; + static constexpr std::array expected = { + std::pair{0x80020000u, 0x3C081234u}, std::pair{0x80020004u, 0x35085678u}, std::pair{0x80020008u, 0xAFA80010u}, + std::pair{0x8002000Cu, 0x8FA2FFFCu}, std::pair{0x80020010u, 0x00481821u}, std::pair{0x80030000u, 0x0C00C400u}, + std::pair{0x80030004u, 0x03E00008u}, + }; + + Error error; + std::unique_ptr base = Cheats::ParseCode(MakeMetadata(Cheats::CodeType::Assembly), input, &error); + ASSERT_NE(base, nullptr) << error.GetDescription(); + ExpectAssemblyInstructions(*static_cast(base.get()), expected); +} + +TEST(Cheats, ParseAssemblyBranchTypes) +{ + static constexpr std::string_view input = ".org 0x80040000\n" + "target: nop\n" + "beq t0, t1, target\n" + "bne t0, t1, target\n" + "blez t0, target\n" + "bgtz t0, target\n" + "bltz t0, target\n" + "bgez t0, target\n" + "bltzal t0, target\n" + "bgezal t0, target\n"; + static constexpr std::array expected = { + std::pair{0x80040000u, 0x00000000u}, std::pair{0x80040004u, 0x1109FFFEu}, std::pair{0x80040008u, 0x1509FFFDu}, + std::pair{0x8004000Cu, 0x1900FFFCu}, std::pair{0x80040010u, 0x1D00FFFBu}, std::pair{0x80040014u, 0x0500FFFAu}, + std::pair{0x80040018u, 0x0501FFF9u}, std::pair{0x8004001Cu, 0x0510FFF8u}, std::pair{0x80040020u, 0x0511FFF7u}, + }; + + Error error; + std::unique_ptr base = Cheats::ParseCode(MakeMetadata(Cheats::CodeType::Assembly), input, &error); + ASSERT_NE(base, nullptr) << error.GetDescription(); + ExpectAssemblyInstructions(*static_cast(base.get()), expected); +} + +TEST(Cheats, ParseAssemblyCodeErrors) +{ + static constexpr std::array cases = { + "nop\n", + ".org 0x80010002\nnop\n", + ".org nope\nnop\n", + ".org 0x80010000\nbeq zero, zero, missing\n", + ".org 0x80010000\nlabel: nop\nlabel: nop\n", + ".org 0x80010000\nnop\n.org 0x80010000\nnop\n", + ".org 0x80010000\nori t0, zero, 0x??0?\n", + ".org 0x80010000\n", + }; + + for (const std::string_view input : cases) + { + Error error; + EXPECT_EQ(Cheats::ParseCode(MakeMetadata(Cheats::CodeType::Assembly), input, &error), nullptr) << input; + EXPECT_TRUE(error.IsValid()) << input; + } +} diff --git a/src/core-tests/core-tests.vcxproj b/src/core-tests/core-tests.vcxproj index 3dc5235a5..4b2943e39 100644 --- a/src/core-tests/core-tests.vcxproj +++ b/src/core-tests/core-tests.vcxproj @@ -3,8 +3,11 @@ + + + @@ -12,6 +15,9 @@ {49953e1b-2ef7-46a4-b88b-1bf9e099093b} + + {bb08260f-6fbc-46af-8924-090ee71360c6} + {ee054e08-3799-4a59-a422-18259c105ffd} @@ -20,13 +26,15 @@ {93D9300C-B64A-4B21-A4B5-30173DE88747} - + %(AdditionalIncludeDirectories);$(SolutionDir)dep\googletest\include + true Console + true diff --git a/src/core-tests/core-tests.vcxproj.filters b/src/core-tests/core-tests.vcxproj.filters index 92485331c..feaa366ba 100644 --- a/src/core-tests/core-tests.vcxproj.filters +++ b/src/core-tests/core-tests.vcxproj.filters @@ -2,8 +2,11 @@ + + + diff --git a/src/core-tests/stub_cpu.cpp b/src/core-tests/stub_cpu.cpp index c6684332c..677ecc375 100644 --- a/src/core-tests/stub_cpu.cpp +++ b/src/core-tests/stub_cpu.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #include "core/cpu_core.h" +#include "core/system.h" CPU::State CPU::g_state; @@ -25,3 +26,32 @@ bool CPU::SafeReadMemoryWord(VirtualMemoryAddress addr, u32* value) *value = 0; return false; } + +bool CPU::SafeWriteMemoryByte(VirtualMemoryAddress addr, u8 value) +{ + return false; +} + +bool CPU::SafeWriteMemoryHalfWord(VirtualMemoryAddress addr, u16 value) +{ + return false; +} + +bool CPU::SafeWriteMemoryWord(VirtualMemoryAddress addr, u32 value) +{ + return false; +} + +void CPU::InvalidateICacheAt(VirtualMemoryAddress address) +{ +} + +u32 System::GetFrameNumber() +{ + return 0; +} + +Controller* System::GetController(u32 slot) +{ + return nullptr; +} diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 1b0acb66b..05a3bbb93 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -24,6 +24,7 @@ add_library(core cdrom_subq_replacement.h cheats.cpp cheats.h + cheats_private.h controller.cpp controller.h controller_helpers.h diff --git a/src/core/cheats.cpp b/src/core/cheats.cpp index 9e601366f..ea29e2eea 100644 --- a/src/core/cheats.cpp +++ b/src/core/cheats.cpp @@ -4,6 +4,7 @@ #include "cheats.h" #include "achievements.h" #include "bus.h" +#include "cheats_private.h" #include "controller.h" #include "core.h" #include "cpu_core.h" @@ -162,50 +163,6 @@ private: namespace Cheats { -namespace { -/// Represents a cheat code, after being parsed. -class CheatCode -{ -public: - /// Additional metadata to a cheat code, present for all types. - struct Metadata - { - std::string name; - CodeType type = CodeType::Gameshark; - CodeActivation activation = CodeActivation::EndFrame; - std::optional override_cpu_overclock; - std::optional override_aspect_ratio; - bool has_options : 1; - bool disable_widescreen_rendering : 1; - bool enable_8mb_ram : 1; - bool disallow_for_achievements : 1; - }; - -public: - explicit CheatCode(Metadata metadata); - virtual ~CheatCode(); - - ALWAYS_INLINE const Metadata& GetMetadata() const { return m_metadata; } - ALWAYS_INLINE const std::string& GetName() const { return m_metadata.name; } - ALWAYS_INLINE CodeActivation GetActivation() const { return m_metadata.activation; } - ALWAYS_INLINE bool IsManuallyActivated() const { return (m_metadata.activation == CodeActivation::Manual); } - ALWAYS_INLINE bool HasOptions() const { return m_metadata.has_options; } - - bool HasAnySettingOverrides() const; - void ApplySettingOverrides(); - - virtual void SetOptionValue(u32 value) = 0; - - virtual void Apply() const = 0; - virtual void ApplyOnDisable(RollbackLog* rollback_list) const = 0; - - virtual bool HasRestorableOnDisableEffects() const = 0; - -protected: - Metadata m_metadata; -}; -} // namespace - using CheatCodeList = std::vector>; using ActiveCodeList = std::vector; using EnableCodeList = std::vector; @@ -238,7 +195,6 @@ static void EnumerateChtFiles(const std::string_view serial, std::optional ParseOption(const std::string_view value); static bool ParseOptionRange(const std::string_view value, u16* out_range_start, u16* out_range_end); static void ParseFile(CheatCodeList* dst_list, const std::string_view file_contents); -static std::unique_ptr ParseCode(CheatCode::Metadata metadata, const std::string_view data, Error* error); static Cheats::FileFormat DetectFileFormat(const std::string_view file_contents); static bool ImportPCSXFile(CodeInfoList* dst, const std::string_view file_contents, bool stop_on_error, Error* error); @@ -2174,122 +2130,6 @@ bool Cheats::ImportOldChtFile(const std::string_view serial) // Gameshark codes ////////////////////////////////////////////////////////////////////////// -namespace Cheats { -namespace { - -class GamesharkCheatCode final : public CheatCode -{ -public: - explicit GamesharkCheatCode(Metadata metadata); - ~GamesharkCheatCode() override; - - static std::unique_ptr Parse(Metadata metadata, const std::string_view data, Error* error); - - void SetOptionValue(u32 value) override; - - void Apply() const override; - void ApplyOnDisable(RollbackLog* rollback_list) const override; - - bool HasRestorableOnDisableEffects() const override; - -private: - 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, - - // 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, - ExtConstantWriteIfMatchWithRestore8 = 0xA8, - ExtConstantForceRange8 = 0xF0, - ExtConstantForceRangeLimits16 = 0xF1, - ExtConstantForceRangeRollRound16 = 0xF2, - ExtConstantForceRange16 = 0xF3, - ExtFindAndReplace = 0xF4, - ExtConstantSwap16 = 0xF5, - - 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 code; - BitField address; - BitField value32; - BitField value16; - BitField value8; - }; - - std::vector instructions; - std::vector> option_instruction_values; - - u32 GetNextNonConditionalInstruction(u32 index) const; - - static bool IsConditionalInstruction(InstructionCode code); -}; - -} // namespace - -} // namespace Cheats - Cheats::GamesharkCheatCode::GamesharkCheatCode(Metadata metadata) : CheatCode(std::move(metadata)) { } @@ -4565,42 +4405,6 @@ bool Cheats::GamesharkCheatCode::HasRestorableOnDisableEffects() const // Assembly codes ////////////////////////////////////////////////////////////////////////// -namespace Cheats { -namespace { - -class AssemblyCheatCode final : public CheatCode -{ -public: - explicit AssemblyCheatCode(Metadata metadata); - ~AssemblyCheatCode() override; - - static std::unique_ptr Parse(Metadata metadata, std::string_view data, Error* error); - - void SetOptionValue(u32 value) override; - - void Apply() const override; - void ApplyOnDisable(RollbackLog* rollback_list) const override; - - bool HasRestorableOnDisableEffects() const override; - -private: - static constexpr u64 UNINITIALIZED_OLD_VALUE = UINT64_C(0xFFFFFFFFFFFFFFFF); - - struct Instruction - { - u32 pc; - u32 new_value; - mutable u64 old_value; // higher order bits are set so fresh codes apply - }; - static_assert(std::is_trivially_copyable_v); - - std::vector instructions; - std::vector> option_instruction_values; -}; - -} // namespace -} // namespace Cheats - Cheats::AssemblyCheatCode::AssemblyCheatCode(Metadata metadata) : CheatCode(std::move(metadata)) { } @@ -4795,6 +4599,11 @@ std::unique_ptr Cheats::AssemblyCheatCode::Parse(Meta return code; } +std::span Cheats::AssemblyCheatCode::GetInstructions() const +{ + return instructions; +} + void Cheats::AssemblyCheatCode::SetOptionValue(u32 value) { for (const auto& [index, bit_position, bit_count] : option_instruction_values) diff --git a/src/core/cheats_private.h b/src/core/cheats_private.h new file mode 100644 index 000000000..72d665a27 --- /dev/null +++ b/src/core/cheats_private.h @@ -0,0 +1,203 @@ +// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin and contributors. +// SPDX-License-Identifier: CC-BY-NC-ND-4.0 + +#pragma once + +#include "cheats.h" + +#include "common/bitfield.h" + +#include +#include +#include + +namespace Cheats { + +/// Represents a cheat code, after being parsed. +class CheatCode +{ +public: + /// Additional metadata to a cheat code, present for all types. + struct Metadata + { + std::string name; + CodeType type = CodeType::Gameshark; + CodeActivation activation = CodeActivation::EndFrame; + std::optional override_cpu_overclock; + std::optional override_aspect_ratio; + bool has_options : 1; + bool disable_widescreen_rendering : 1; + bool enable_8mb_ram : 1; + bool disallow_for_achievements : 1; + }; + +public: + explicit CheatCode(Metadata metadata); + virtual ~CheatCode(); + + ALWAYS_INLINE const Metadata& GetMetadata() const { return m_metadata; } + ALWAYS_INLINE const std::string& GetName() const { return m_metadata.name; } + ALWAYS_INLINE CodeActivation GetActivation() const { return m_metadata.activation; } + ALWAYS_INLINE bool IsManuallyActivated() const { return (m_metadata.activation == CodeActivation::Manual); } + ALWAYS_INLINE bool HasOptions() const { return m_metadata.has_options; } + + bool HasAnySettingOverrides() const; + void ApplySettingOverrides(); + + virtual void SetOptionValue(u32 value) = 0; + + virtual void Apply() const = 0; + virtual void ApplyOnDisable(RollbackLog* rollback_list) const = 0; + + virtual bool HasRestorableOnDisableEffects() const = 0; + +protected: + Metadata m_metadata; +}; + +class GamesharkCheatCode final : public CheatCode +{ +public: + 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, + + // 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, + ExtConstantWriteIfMatchWithRestore8 = 0xA8, + ExtConstantForceRange8 = 0xF0, + ExtConstantForceRangeLimits16 = 0xF1, + ExtConstantForceRangeRollRound16 = 0xF2, + ExtConstantForceRange16 = 0xF3, + ExtFindAndReplace = 0xF4, + ExtConstantSwap16 = 0xF5, + + 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 code; + BitField address; + BitField value32; + BitField value16; + BitField value8; + }; + + explicit GamesharkCheatCode(Metadata metadata); + ~GamesharkCheatCode() override; + + static std::unique_ptr Parse(Metadata metadata, const std::string_view data, Error* error); + + std::span GetInstructions() const; + + void SetOptionValue(u32 value) override; + + void Apply() const override; + void ApplyOnDisable(RollbackLog* rollback_list) const override; + + bool HasRestorableOnDisableEffects() const override; + +private: + std::vector instructions; + std::vector> option_instruction_values; + + u32 GetNextNonConditionalInstruction(u32 index) const; + + static bool IsConditionalInstruction(InstructionCode code); +}; + +class AssemblyCheatCode final : public CheatCode +{ +public: + static constexpr u64 UNINITIALIZED_OLD_VALUE = UINT64_C(0xFFFFFFFFFFFFFFFF); + + struct Instruction + { + u32 pc; + u32 new_value; + mutable u64 old_value; // higher order bits are set so fresh codes apply + }; + static_assert(std::is_trivially_copyable_v); + + explicit AssemblyCheatCode(Metadata metadata); + ~AssemblyCheatCode() override; + + static std::unique_ptr Parse(Metadata metadata, std::string_view data, Error* error); + + std::span GetInstructions() const; + + void SetOptionValue(u32 value) override; + + void Apply() const override; + void ApplyOnDisable(RollbackLog* rollback_list) const override; + + bool HasRestorableOnDisableEffects() const override; + +private: + std::vector instructions; + std::vector> option_instruction_values; +}; + +std::unique_ptr ParseCode(CheatCode::Metadata metadata, std::string_view data, Error* error); + +} // namespace Cheats diff --git a/src/core/core.vcxproj b/src/core/core.vcxproj index 5da86b75b..4ea702488 100644 --- a/src/core/core.vcxproj +++ b/src/core/core.vcxproj @@ -95,6 +95,7 @@ + diff --git a/src/core/core.vcxproj.filters b/src/core/core.vcxproj.filters index 60a4fe431..1f109566a 100644 --- a/src/core/core.vcxproj.filters +++ b/src/core/core.vcxproj.filters @@ -112,6 +112,7 @@ + diff --git a/src/core/system.h b/src/core/system.h index 863845f9d..93f1138c9 100644 --- a/src/core/system.h +++ b/src/core/system.h @@ -8,6 +8,8 @@ #include "util/image.h" +#include +#include #include #include #include