Cheats: Expand test coverage

This commit is contained in:
Stenzek
2026-07-03 17:27:50 +10:00
parent 7bed00d02d
commit 36a85ffbfc
11 changed files with 560 additions and 199 deletions

View File

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

View File

@@ -0,0 +1,292 @@
// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#include "core/cheats_private.h"
#include "common/error.h"
#include <array>
#include <gtest/gtest.h>
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<const std::pair<u32, u32>> 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<Cheats::CheatCode> base = Cheats::ParseCode(MakeMetadata(Cheats::CodeType::Gameshark), input, &error);
ASSERT_NE(base, nullptr) << error.GetDescription();
Cheats::GamesharkCheatCode* code = static_cast<Cheats::GamesharkCheatCode*>(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<Cheats::CheatCode> 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<Cheats::GamesharkCheatCode*>(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<std::string_view, 4> 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<Cheats::CheatCode> base = Cheats::ParseCode(std::move(metadata), input, &error);
ASSERT_NE(base, nullptr) << error.GetDescription();
Cheats::AssemblyCheatCode* code = static_cast<Cheats::AssemblyCheatCode*>(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<Cheats::CheatCode> base = Cheats::ParseCode(MakeMetadata(Cheats::CodeType::Assembly), input, &error);
ASSERT_NE(base, nullptr) << error.GetDescription();
ExpectAssemblyInstructions(*static_cast<Cheats::AssemblyCheatCode*>(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<Cheats::CheatCode> base = Cheats::ParseCode(MakeMetadata(Cheats::CodeType::Assembly), input, &error);
ASSERT_NE(base, nullptr) << error.GetDescription();
ExpectAssemblyInstructions(*static_cast<Cheats::AssemblyCheatCode*>(base.get()), expected);
}
TEST(Cheats, ParseAssemblyCodeErrors)
{
static constexpr std::array<std::string_view, 8> 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;
}
}

View File

@@ -3,8 +3,11 @@
<Import Project="..\..\dep\vsprops\Configurations.props" />
<ItemGroup>
<ClCompile Include="..\..\dep\googletest\src\gtest_main.cc" />
<ClCompile Include="..\core\cheats.cpp" />
<ClCompile Include="..\core\cpu_disasm.cpp" />
<ClCompile Include="..\core\cpu_types.cpp" />
<ClCompile Include="..\util\zip_helpers.cpp" />
<ClCompile Include="cheats_tests.cpp" />
<ClCompile Include="cpu_disasm_tests.cpp" />
<ClCompile Include="stub_cpu.cpp" />
</ItemGroup>
@@ -12,6 +15,9 @@
<ProjectReference Include="..\..\dep\googletest\googletest.vcxproj">
<Project>{49953e1b-2ef7-46a4-b88b-1bf9e099093b}</Project>
</ProjectReference>
<ProjectReference Include="..\..\dep\imgui\imgui.vcxproj">
<Project>{bb08260f-6fbc-46af-8924-090ee71360c6}</Project>
</ProjectReference>
<ProjectReference Include="..\common\common.vcxproj">
<Project>{ee054e08-3799-4a59-a422-18259c105ffd}</Project>
</ProjectReference>
@@ -20,13 +26,15 @@
<ProjectGuid>{93D9300C-B64A-4B21-A4B5-30173DE88747}</ProjectGuid>
</PropertyGroup>
<Import Project="..\..\dep\vsprops\ConsoleApplication.props" />
<Import Project="..\common\common.props" />
<Import Project="..\util\util.props" />
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>%(AdditionalIncludeDirectories);$(SolutionDir)dep\googletest\include</AdditionalIncludeDirectories>
<FunctionLevelLinking>true</FunctionLevelLinking>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<OptimizeReferences>true</OptimizeReferences>
</Link>
</ItemDefinitionGroup>
<Import Project="..\..\dep\vsprops\Targets.props" />

View File

@@ -2,8 +2,11 @@
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<ClCompile Include="..\..\dep\googletest\src\gtest_main.cc" />
<ClCompile Include="..\core\cheats.cpp" />
<ClCompile Include="..\core\cpu_disasm.cpp" />
<ClCompile Include="..\core\cpu_types.cpp" />
<ClCompile Include="..\util\zip_helpers.cpp" />
<ClCompile Include="cheats_tests.cpp" />
<ClCompile Include="cpu_disasm_tests.cpp" />
<ClCompile Include="stub_cpu.cpp" />
</ItemGroup>

View File

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

View File

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

View File

@@ -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<u32> override_cpu_overclock;
std::optional<DisplayAspectRatio> 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<std::unique_ptr<CheatCode>>;
using ActiveCodeList = std::vector<const CheatCode*>;
using EnableCodeList = std::vector<std::string>;
@@ -238,7 +195,6 @@ static void EnumerateChtFiles(const std::string_view serial, std::optional<GameH
static std::optional<CodeOption> 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<CheatCode> 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<GamesharkCheatCode> 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<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::vector<Instruction> instructions;
std::vector<std::tuple<u32, u8, u8>> 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<AssemblyCheatCode> 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<Instruction>);
std::vector<Instruction> instructions;
std::vector<std::tuple<u32, u8, u8>> option_instruction_values;
};
} // namespace
} // namespace Cheats
Cheats::AssemblyCheatCode::AssemblyCheatCode(Metadata metadata) : CheatCode(std::move(metadata))
{
}
@@ -4795,6 +4599,11 @@ std::unique_ptr<Cheats::AssemblyCheatCode> Cheats::AssemblyCheatCode::Parse(Meta
return code;
}
std::span<const Cheats::AssemblyCheatCode::Instruction> Cheats::AssemblyCheatCode::GetInstructions() const
{
return instructions;
}
void Cheats::AssemblyCheatCode::SetOptionValue(u32 value)
{
for (const auto& [index, bit_position, bit_count] : option_instruction_values)

203
src/core/cheats_private.h Normal file
View File

@@ -0,0 +1,203 @@
// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <stenzek@gmail.com> and contributors.
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#pragma once
#include "cheats.h"
#include "common/bitfield.h"
#include <cstdint>
#include <span>
#include <tuple>
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<u32> override_cpu_overclock;
std::optional<DisplayAspectRatio> 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<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;
};
explicit GamesharkCheatCode(Metadata metadata);
~GamesharkCheatCode() override;
static std::unique_ptr<GamesharkCheatCode> Parse(Metadata metadata, const std::string_view data, Error* error);
std::span<const Instruction> 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<Instruction> instructions;
std::vector<std::tuple<u32, u8, u8>> 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<Instruction>);
explicit AssemblyCheatCode(Metadata metadata);
~AssemblyCheatCode() override;
static std::unique_ptr<AssemblyCheatCode> Parse(Metadata metadata, std::string_view data, Error* error);
std::span<const Instruction> 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<Instruction> instructions;
std::vector<std::tuple<u32, u8, u8>> option_instruction_values;
};
std::unique_ptr<CheatCode> ParseCode(CheatCode::Metadata metadata, std::string_view data, Error* error);
} // namespace Cheats

View File

@@ -95,6 +95,7 @@
<ClInclude Include="cdrom_async_reader.h" />
<ClInclude Include="cdrom_subq_replacement.h" />
<ClInclude Include="cheats.h" />
<ClInclude Include="cheats_private.h" />
<ClInclude Include="achievements.h" />
<ClInclude Include="controller_helpers.h" />
<ClInclude Include="core.h" />

View File

@@ -112,6 +112,7 @@
<ClInclude Include="cpu_pgxp.h" />
<ClInclude Include="cpu_core_private.h" />
<ClInclude Include="cheats.h" />
<ClInclude Include="cheats_private.h" />
<ClInclude Include="memory_card_image.h" />
<ClInclude Include="analog_joystick.h" />
<ClInclude Include="gpu_types.h" />

View File

@@ -8,6 +8,8 @@
#include "util/image.h"
#include <ctime>
#include <functional>
#include <memory>
#include <optional>
#include <span>