From 3e175060dd745867be4dcc79ccd88d6864a80c26 Mon Sep 17 00:00:00 2001 From: LunaMoo Date: Thu, 15 Oct 2026 18:27:33 +0200 Subject: [PATCH 1/2] Cheats: Support PSPAR (_M) codes PSPAR code types run the way TempAR's cheat_apply_pspar() runs them: writes, 32-bit and masked 16-bit conditions with the D0 terminator stack, load/set/add/write offset, C0 loops with D1/D2, the Dx data register codes, patch (0x0E), memory copy (0x0F) and the safe data store (0xC4), which lets a cheat write over its own lines. PSPAR's button codes (type 9/A at 0x09F00000 and up) read the real pad, like TempAR's hijack option, as does TempAR's fake pad address 0x0A000000. TempAR's _N extensions aren't supported, and folder codes (0xCF) do nothing. Lines can now end in a # or // comment, a cheat can't mix _L and _M lines, and importing from cheat.db keeps _M lines. The unit test runs the PSPAR tests from TempAR's tests.db. Co-Authored-By: Claude Opus 5.5 --- CMakeLists.txt | 1 + Core/CwCheat.cpp | 350 ++++++++++++++++++++++++++++- Core/CwCheat.h | 18 +- UI/CwCheatScreen.cpp | 2 +- android/jni/Android.mk | 1 + unittest/TestCwCheat.cpp | 211 +++++++++++++++++ unittest/UnitTest.cpp | 2 + unittest/UnitTests.vcxproj | 1 + unittest/UnitTests.vcxproj.filters | 1 + 9 files changed, 578 insertions(+), 9 deletions(-) create mode 100644 unittest/TestCwCheat.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index f0c3b7c64b..dfdf4c11c5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1393,6 +1393,7 @@ if(UNITTEST) unittest/TestVFS.cpp unittest/TestZipSlip.cpp unittest/TestLzrc.cpp + unittest/TestCwCheat.cpp unittest/TestMpegCsc.cpp unittest/TestSplineTessellation.cpp unittest/TestGEMath.cpp diff --git a/Core/CwCheat.cpp b/Core/CwCheat.cpp index 704b875984..3be8712a7d 100644 --- a/Core/CwCheat.cpp +++ b/Core/CwCheat.cpp @@ -4,6 +4,7 @@ #include #include "Common/Data/Text/I18n.h" +#include "Common/LogReporting.h" #include "Common/StringUtils.h" #include "Common/Serialize/Serializer.h" #include "Common/Serialize/SerializeFuncs.h" @@ -93,7 +94,7 @@ bool CheatFileParser::Parse() { void CheatFileParser::Flush() { if (!pendingLines_.empty()) { - cheats_.push_back(CheatCode{lastCheatInfo_.name, pendingLines_}); + cheats_.push_back(CheatCode{lastCheatInfo_.name, pendingLines_, pendingFormat_}); FlushCheatInfo(); pendingLines_.clear(); } @@ -164,12 +165,17 @@ void CheatFileParser::ParseLine(const std::string &line, int lineNumber) { case 'L': // CwCheat data line. - ParseDataLine(line.substr(2), lineNumber); + ParseDataLine(line.substr(2), lineNumber, CheatCodeFormat::CwCheat); return; case 'M': - // TempAR data line. - AddError("TempAR codes not supported", lineNumber); + // PSPAR data line (TempAR's name for it). + ParseDataLine(line.substr(2), lineNumber, CheatCodeFormat::PSPAR); + return; + + case 'N': + // TempAR's own extensions to the PSPAR code types. + AddError("TempAR extended (_N) codes not supported", lineNumber); return; default: @@ -178,7 +184,7 @@ void CheatFileParser::ParseLine(const std::string &line, int lineNumber) { } } -void CheatFileParser::ParseDataLine(const std::string &line, int lineNumber) { +void CheatFileParser::ParseDataLine(const std::string &line, int lineNumber, CheatCodeFormat format) { if (!gameEnabled_) { return; } @@ -188,10 +194,18 @@ void CheatFileParser::ParseDataLine(const std::string &line, int lineNumber) { return; } + if (pendingLines_.empty()) { + pendingFormat_ = format; + } else if (pendingFormat_ != format) { + AddError("can't mix _L and _M lines in one cheat", lineNumber); + return; + } + CheatLine cheatLine; int len = 0; if (sscanf(line.c_str(), "%x %x %n", &cheatLine.part1, &cheatLine.part2, &len) == 2) { - if ((size_t)len < line.length()) { + // A trailing comment is fine (TempAR's databases use them). + if ((size_t)len < line.length() && line[len] != '#' && line.compare(len, 2, "//") != 0) { AddError("junk after line data", lineNumber); } pendingLines_.push_back(cheatLine); @@ -1123,12 +1137,334 @@ void CWCheatEngine::ExecuteOp(const CheatOperation &op, const CheatCode &cheat, } } +// PSPAR (PSP Action Replay) code types. +// +// This follows cheat_apply_pspar() in TempAR (src/objects/cheat.c), the plugin most PSPAR codes are run +// with now, rather than Datel's documentation, which is unclear in places. TempAR's own extensions (_N +// lines, CHEAT_PSPAR_EXT there) aren't supported: 0xC1, 0xC2 and 0xC5 do nothing, and 0xD4 always adds. + +// Where a cheat's own lines appear to its code, so the safe data store (0xC4) can point the offset at +// them. On a PSP that's wherever TempAR keeps the cheat in memory. This is above all PSP memory, so +// it's never translated as a relative address. +static const u32 PSPAR_SELF_BASE = 0xF0000000; +// TempAR reads the pad from fake addresses: buttons at +0, analog X at +4, analog Y at +8. +static const u32 PSPAR_PAD_BASE = 0x0A000000; +// Hard cap on the lines one cheat runs per pass, as a loop (0xC0) can repeat billions of times. +static const u32 PSPAR_MAX_LINES_PER_RUN = 0x400000; + +// Addresses below user memory are relative to 0x08800000, like in CwCheat. Others are used as is. +static u32 PSPARResolveAddress(u32 addr) { + if (addr < 0x08800000) { + addr &= 0x0FFFFFFF; + if (addr <= 0x017FFFFF) { + addr += 0x08800000; + } + } + return addr; +} + +static u32 PSPARPadValue(u32 addr) { + u32 value = 0; + switch (addr & ~3) { + case PSPAR_PAD_BASE: + value = __CtrlPeekButtons(); + break; + case PSPAR_PAD_BASE + 4: + case PSPAR_PAD_BASE + 8: + { + float x, y; + __CtrlPeekAnalog(CTRL_STICK_LEFT, &x, &y); + // The inverse of __CtrlPeekAnalog's scaling. + float v = (addr & ~3) == PSPAR_PAD_BASE + 4 ? x : -y; + value = (u32)std::clamp((int)(v * 127.5f + 128.0f), 0, 255); + } + break; + } + return value >> (8 * (addr & 3)); +} + +static u32 SizeMask(int size) { + return size == 4 ? 0xFFFFFFFF : (1U << (size * 8)) - 1; +} + +u32 CWCheatEngine::PSPARRead(const CheatCode &cheat, u32 addr, int size) const { + if ((addr & 0xFF000000) == PSPAR_PAD_BASE) { + return PSPARPadValue(addr) & SizeMask(size); + } + + addr = PSPARResolveAddress(addr) & ~(size - 1); + if (addr >= PSPAR_SELF_BASE) { + u32 offset = addr - PSPAR_SELF_BASE; + if (offset / 8 >= cheat.lines.size()) { + return 0; + } + const CheatLine &line = cheat.lines[offset / 8]; + u32 word = (offset & 4) ? line.part2 : line.part1; + return (word >> (8 * (offset & 3))) & SizeMask(size); + } + + addr &= 0x3FFFFFFF; + if (!Memory::IsValidRange(addr, size)) { + return 0; + } + if (size == 1) + return Memory::ReadUnchecked_U8(addr); + if (size == 2) + return Memory::ReadUnchecked_U16(addr); + return Memory::ReadUnchecked_U32(addr); +} + +void CWCheatEngine::PSPARWrite(CheatCode &cheat, u32 addr, u32 value, int size) const { + addr = PSPARResolveAddress(addr) & ~(size - 1); + if (addr >= PSPAR_SELF_BASE) { + u32 offset = addr - PSPAR_SELF_BASE; + if (offset / 8 >= cheat.lines.size()) { + return; + } + CheatLine &line = cheat.lines[offset / 8]; + u32 &word = (offset & 4) ? line.part2 : line.part1; + const int shift = 8 * (offset & 3); + word = (word & ~(SizeMask(size) << shift)) | ((value & SizeMask(size)) << shift); + return; + } + + addr &= 0x3FFFFFFF; + if (!Memory::IsValidRange(addr, size)) { + return; + } + InvalidateICache(addr, size); + if (size == 1) + Memory::WriteUnchecked_U8((u8)value, addr); + else if (size == 2) + Memory::WriteUnchecked_U16((u16)value, addr); + else + Memory::WriteUnchecked_U32(value, addr); +} + +void CWCheatEngine::RunPSPAR(CheatCode &cheat) { + // The "Dx" registers. Bit 0 of the execution status set means the codes are skipped, and each + // condition pushes a bit that a terminator (0xD0) pops. + u32 offset = 0; + u32 repeatValue = 0; + size_t repeatBegin = 0; + u32 dataRegister = 0; + u32 executionStatus = 0; + u32 executionStatusOld = 0; + + const size_t count = cheat.lines.size(); + u32 linesRun = 0; + for (size_t i = 0; i < count; i++) { + if (++linesRun > PSPAR_MAX_LINES_PER_RUN) { + WARN_LOG_ONCE(psparLoop, Log::System, "PSPAR cheat '%s' runs too many lines, stopping it", cheat.name.c_str()); + break; + } + + const CheatLine &line = cheat.lines[i]; + u32 type; + u32 addr; + if ((line.part1 >> 28) == 0xC || (line.part1 >> 28) == 0xD) { + type = line.part1 >> 24; + addr = line.part1 & 0x00FFFFFF; + } else { + type = line.part1 >> 28; + addr = line.part1 & 0x0FFFFFFF; + } + u32 value = line.part2; + + // Conditions and terminators run even while codes are being skipped. + const bool isCondition = type >= 0x03 && type <= 0x0A; + if ((executionStatus & 1) == 0 || isCondition || type == 0xD0 || type == 0xD1 || type == 0xD2) { + switch (type) { + case 0x00: // 32-bit write + PSPARWrite(cheat, addr + offset, value, 4); + break; + case 0x01: // 16-bit write + PSPARWrite(cheat, addr + offset, value, 2); + break; + case 0x02: // 8-bit write + PSPARWrite(cheat, addr + offset, value, 1); + break; + + case 0x03: case 0x04: case 0x05: case 0x06: // 32-bit conditions + case 0x07: case 0x08: case 0x09: case 0x0A: // 16-bit conditions with a mask + { + bool skip = false; + // PSPAR's own button codes read its pad buffer, at the top of user memory. TempAR + // can send them to the real pad instead, which is all we can do. + if (addr >= 0x09F00000 && (type == 0x09 || type == 0x0A)) { + if ((value & 0xFFFF) != 0) { + addr = PSPAR_PAD_BASE; + value = (value & 0xFFFF) | (~value << 16); + } else { + skip = true; + } + } + if (addr == 0) { + addr = offset; + } + + bool pass = true; + if (!skip && (executionStatus & 1) == 0) { + if (type <= 0x06) { + const u32 memValue = PSPARRead(cheat, addr, 4); + switch (type) { + case 0x03: pass = value > memValue; break; + case 0x04: pass = value < memValue; break; + case 0x05: pass = value == memValue; break; + case 0x06: pass = value != memValue; break; + } + } else { + const u32 compare = value & 0xFFFF; + const u32 memValue = (~value >> 16) & PSPARRead(cheat, addr, 2); + switch (type) { + case 0x07: pass = compare > memValue; break; + case 0x08: pass = compare < memValue; break; + case 0x09: pass = compare == memValue; break; + case 0x0A: pass = compare != memValue; break; + } + } + } + if (!skip) { + executionStatus = (executionStatus << 1) | (((executionStatus & 1) != 0 || !pass) ? 1 : 0); + } + } + break; + + case 0x0B: // Load offset + offset = PSPARRead(cheat, addr + offset, 4); + break; + + case 0x0E: // Patch: copies the bytes in the lines after this one + { + const u32 dest = PSPARResolveAddress(addr); + const u32 src = PSPAR_SELF_BASE + (u32)(i + 1) * 8; + for (u32 b = 0; b < value && (i + 1) * 8 + b < count * 8; b++) { + PSPARWrite(cheat, dest + b, PSPARRead(cheat, src + b, 1), 1); + } + } + break; + + case 0x0F: // Memory copy from the offset + { + const u32 dest = PSPARResolveAddress(addr) & 0x3FFFFFFF; + u32 src = offset; + if (src < PSPAR_SELF_BASE) { + // TempAR masks the source like an address in a code. + src = PSPARResolveAddress(src & 0x0FFFFFFF); + } + if (src < PSPAR_SELF_BASE && Memory::IsValidRange(dest, value) && Memory::IsValidRange(src & 0x3FFFFFFF, value)) { + InvalidateICache(dest, value); + Memory::Memcpy(dest, src & 0x3FFFFFFF, value, "PSPAR"); + } else if (src >= PSPAR_SELF_BASE) { + for (u32 b = 0; b < value && b < count * 8; b++) { + PSPARWrite(cheat, dest + b, PSPARRead(cheat, src + b, 1), 1); + } + } + } + break; + + case 0xC0: // Loop + repeatValue = value; + repeatBegin = i; + executionStatusOld = executionStatus; + break; + + case 0xC4: // Safe data store: the offset points at this line + offset = PSPAR_SELF_BASE + (u32)i * 8; + break; + + case 0xC6: // Write offset + PSPARWrite(cheat, value, offset, 4); + break; + + case 0xD0: // Terminator + executionStatus >>= 1; + break; + + case 0xD1: // Loop execute, keeping the Dx registers + case 0xD2: // Loop execute / full terminator + if (repeatValue > 0) { + executionStatus = executionStatusOld; + i = repeatBegin; + repeatValue--; + } else if (type == 0xD1) { + executionStatus = 0; + } else { + executionStatusOld = 0; + executionStatus = 0; + offset = 0; + dataRegister = 0; + repeatBegin = 0; + } + break; + + case 0xD3: // Set offset + offset = value; + break; + case 0xD4: // Add to the data register + dataRegister += value; + break; + case 0xD5: // Set the data register + dataRegister = value; + break; + case 0xD6: // 32-bit incrementing write + PSPARWrite(cheat, value + offset, dataRegister, 4); + offset += 4; + break; + case 0xD7: // 16-bit incrementing write + PSPARWrite(cheat, value + offset, dataRegister, 2); + offset += 2; + break; + case 0xD8: // 8-bit incrementing write + PSPARWrite(cheat, value + offset, dataRegister, 1); + offset += 1; + break; + case 0xD9: // 32-bit load + dataRegister = PSPARRead(cheat, value + offset, 4); + break; + case 0xDA: // 16-bit load + dataRegister = PSPARRead(cheat, value + offset, 2); + break; + case 0xDB: // 8-bit load + dataRegister = PSPARRead(cheat, value + offset, 1); + break; + case 0xDC: // Add to the offset + offset += value; + break; + + default: + // 0xCF (folders and comments) and the TempAR extensions. + break; + } + } + + // Data lines of a patch (and of TempAR's 0xC2) are never run as codes, nor are 0xC1's arguments. + if (type == 0x0E || type == 0xC2) { + i += value / 8 + (value % 8 != 0 ? 1 : 0); + } else if (type == 0xC1) { + i += 2; + } + + // At the end of the cheat, a pending loop repeats without a terminator. + if (i + 1 >= count && repeatValue > 0) { + executionStatus = executionStatusOld; + i = repeatBegin; + repeatValue--; + } + } +} + void CWCheatEngine::Run() { if (Achievements::HardcoreModeActive()) { return; } - for (const CheatCode &cheat : cheats_) { + for (CheatCode &cheat : cheats_) { + if (cheat.format == CheatCodeFormat::PSPAR) { + RunPSPAR(cheat); + continue; + } + // InterpretNextOp and ExecuteOp move i. for (size_t i = 0; i < cheat.lines.size(); ) { CheatOperation op = InterpretNextCwCheat(cheat, i); diff --git a/Core/CwCheat.h b/Core/CwCheat.h index 3f8c22a655..e8e82b7069 100644 --- a/Core/CwCheat.h +++ b/Core/CwCheat.h @@ -28,9 +28,15 @@ struct CheatLine { bool DetectCheatTitle(std::string_view name, std::string_view *title); bool DetectCheatPostComment(std::string_view name, std::string_view *title); +enum class CheatCodeFormat { + CwCheat, // _L lines + PSPAR, // _M lines (Datel's PSP Action Replay code types, as run by TempAR) +}; + struct CheatCode { std::string name; std::vector lines; + CheatCodeFormat format = CheatCodeFormat::CwCheat; }; struct CheatFileInfo { @@ -59,6 +65,10 @@ public: } void Run(); bool HasCheats(); + // Replaces the parsed cheats, for tests. + void SetCheats(const std::vector &cheats) { + cheats_ = cheats; + } static u32 GetAddress(u32 value) { // TODO: This comment is weird: // Returns static address used by ppsspp. Some games may not like this, and causes cheats to not work without offset @@ -76,6 +86,11 @@ private: inline bool TestIf(const CheatOperation &op, bool(*oper)(int a, int b)) const; inline bool TestIfAddr(const CheatOperation &op, bool(*oper)(int a, int b)) const; + // Takes the cheat non-const: PSPAR's safe data store (0xC4) lets a code write over its own lines. + void RunPSPAR(CheatCode &cheat); + u32 PSPARRead(const CheatCode &cheat, u32 addr, int size) const; + void PSPARWrite(CheatCode &cheat, u32 addr, u32 value, int size) const; + std::vector cheats_; std::string gameID_; Path filename_; @@ -97,7 +112,7 @@ protected: void FlushCheatInfo(); void AddError(const std::string &msg, int lineNumber); void ParseLine(const std::string &line, int lineNumber); - void ParseDataLine(const std::string &line, int lineNumber); + void ParseDataLine(const std::string &line, int lineNumber, CheatCodeFormat format); bool ValidateGameID(std::string_view gameID); FILE *fp_ = nullptr; @@ -108,6 +123,7 @@ protected: std::vector cheatInfo_; std::vector cheats_; std::vector pendingLines_; + CheatCodeFormat pendingFormat_ = CheatCodeFormat::CwCheat; CheatFileInfo lastCheatInfo_; bool gameEnabled_ = true; bool gameRiskyEnabled_ = false; diff --git a/UI/CwCheatScreen.cpp b/UI/CwCheatScreen.cpp index efdcf97eff..bab863f56a 100644 --- a/UI/CwCheatScreen.cpp +++ b/UI/CwCheatScreen.cpp @@ -508,7 +508,7 @@ bool CwCheatScreen::ImportCheats(const Path &cheatFile, int *cheatsFound) { if (line[0] == '_' && (line[1] == 'S' || line[1] == 'G') && title.size() < 2) { title.push_back(line); - } else if (parseCheatEntry && ((line[0] == '_' && (line[1] == 'C' || line[1] == 'L')) || line[0] == '/' || line[0] == '#')) { + } else if (parseCheatEntry && ((line[0] == '_' && (line[1] == 'C' || line[1] == 'L' || line[1] == 'M')) || line[0] == '/' || line[0] == '#')) { newList.push_back(line); (*cheatsFound)++; } diff --git a/android/jni/Android.mk b/android/jni/Android.mk index 524a2b5979..71372b8dd4 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -1108,6 +1108,7 @@ ifeq ($(UNITTEST),1) $(SRC)/unittest/TestVFS.cpp \ $(SRC)/unittest/TestDemangle.cpp \ $(SRC)/unittest/TestLzrc.cpp \ + $(SRC)/unittest/TestCwCheat.cpp \ $(SRC)/unittest/TestMpegCsc.cpp \ $(SRC)/unittest/TestSplineTessellation.cpp \ $(SRC)/unittest/TestGEMath.cpp \ diff --git a/unittest/TestCwCheat.cpp b/unittest/TestCwCheat.cpp new file mode 100644 index 0000000000..917cd0bd55 --- /dev/null +++ b/unittest/TestCwCheat.cpp @@ -0,0 +1,211 @@ +// Copyright (c) 2012- PPSSPP Project. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 2.0 or later versions. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +#include +#include +#include + +#include "Common/File/FileUtil.h" +#include "Common/File/Path.h" +#include "Core/CwCheat.h" +#include "Core/MemMap.h" +#include "Core/MemMapHelpers.h" +#include "Core/HLE/sceCtrl.h" +#include "UnitTest.h" + +// Most of these are the PSPAR tests from TempAR's docs/tests/tests.db, with their expected results. +static const char *const g_cheatFile = R"(_S TEST00000 +_G Cheat tests +_C1 Loop +_M 0xD5000000 0x11223344 +_M 0xC0000000 0x00000002 +_M 0xD6000000 0x08802000 +_M 0xD4000000 0x44444444 +_M 0xD2000000 0x00000000 +_C1 Loop without terminator +_M 0xD5000000 0x11223344 +_M 0xC0000000 0x00000002 +_M 0xD6000000 0x08802000 +_M 0xD4000000 0x44444444 +_C1 Patch +_M 0xE8802000 0x0000000C +_M 0x11223344 0x55667788 +_M 0x99AABBCC 0x00000000 +_C1 Patch with extra lines +_M 0xE8802000 0x0000000C # Expected Result: +_M 0x11223344 0x55667788 # 0x08802000 = 0x11223344; 0x08802004 = 0x55667788 +_M 0x99AABBCC 0x00000000 # 0x08802008 = 0x99AABBCC; 0x0880200C = 0xDDEEFF00 +_M 0x0880200C 0xDDEEFF00 # +_C1 Memory copy +_M 0xD3000000 0x08802000 +_M 0x00000000 0x11223344 +_M 0xF8802008 0x00000004 +_C1 Write offset +_M 0x08802000 0x08802004 +_M 0x08802004 0x08802020 +_M 0x68802000 0x00000000 +_M 0xB8802000 0x00000000 +_M 0xB0000000 0x00000000 +_M 0xC6000000 0x08802008 +_M 0xD2000000 0x00000000 +_C1 Multi-level pointer +_M 0x08802000 0x08802004 +_M 0x08802004 0x08802020 +_M 0x68802000 0x00000000 +_M 0xB8802000 0x00000000 +_M 0xB0000000 0x00000000 +_M 0x00000000 0x12345678 +_M 0xD2000000 0x00000000 +_C1 Data register add +_M 0x08802000 0x11111111 +_M 0xD9000000 0x08802000 +_M 0xD4000000 0x11111111 +_M 0xD6000000 0x08802004 +_M 0xD2000000 0x00000000 +_C1 Data register sub +_M 0x08802000 0x22222222 +_M 0xD9000000 0x08802000 +_M 0xD4000000 0xEEEEEEEF +_M 0xD6000000 0x08802004 +_M 0xD2000000 0x00000000 +_C1 Safe data store +_M 0xC4000000 0x00000000 +_M 0x00000020 0x08802000 +_M 0x00000024 0x12345678 +_M 0xD2000000 0x00000000 +_M 0x00000000 0x00000000 +_C1 Conditions and terminator +_M 0x08802000 0x00001234 +_M 0x58802000 0x00001234 +_M 0x08802004 0x11111111 +_M 0x58802000 0x00005678 +_M 0x08802008 0x22222222 +_M 0xD0000000 0x00000000 +_M 0x0880200C 0x33333333 +_M 0xD0000000 0x00000000 +_M 0x88802000 0xFF0F0005 +_M 0x08802010 0x44444444 +_M 0xD2000000 0x00000000 +_M 0x28802014 0x00000055 +_C1 Pad (PSPAR address) +_M 0x99F00000 0x00000200 +_M 0x08802000 0x11111111 +_M 0xD0000000 0x00000000 +_M 0xA9F00000 0x00000200 +_M 0x08802004 0x22222222 +_C1 Pad (TempAR address) +_M 0x9A000000 0xFDFF0200 +_M 0x08802000 0x33333333 +_C1 CwCheat with a comment +_L 0x20002000 0x11111111 # Comment +)"; + +struct ExpectedWord { + u32 addr; + u32 value; +}; + +static bool RunCheatTest(CWCheatEngine &engine, const CheatCode &cheat, const std::vector &expected) { + Memory::Memset(0x08801F00, 0, 0x400, "CheatTest"); + engine.SetCheats({ cheat }); + engine.Run(); + for (const ExpectedWord &e : expected) { + u32 actual = Memory::ReadUnchecked_U32(e.addr); + if (actual != e.value) { + printf("Cheat '%s': %08x = %08x, expected %08x\n", cheat.name.c_str(), e.addr, actual, e.value); + return false; + } + } + return true; +} + +static bool RunCheatTests() { + const Path filename("unittest_cheats.ini"); + if (!File::WriteStringToFile(true, g_cheatFile, filename)) { + printf("Can't write %s\n", filename.c_str()); + return false; + } + CheatFileParser parser(filename, "TEST00000"); + const bool parsed = parser.Parse(); + for (const std::string &error : parser.GetErrors()) { + printf("%s\n", error.c_str()); + } + const std::vector cheats = parser.GetCheats(); + EXPECT_TRUE(parsed); + EXPECT_EQ_INT((int)cheats.size(), 14); + + const std::vector> expected = { + { { 0x08802000, 0x11223344 }, { 0x08802004, 0x55667788 }, { 0x08802008, 0x99AABBCC } }, + { { 0x08802000, 0x11223344 }, { 0x08802004, 0x55667788 }, { 0x08802008, 0x99AABBCC } }, + { { 0x08802000, 0x11223344 }, { 0x08802004, 0x55667788 }, { 0x08802008, 0x99AABBCC }, { 0x0880200C, 0 } }, + { { 0x08802000, 0x11223344 }, { 0x08802004, 0x55667788 }, { 0x08802008, 0x99AABBCC }, { 0x0880200C, 0xDDEEFF00 } }, + { { 0x08802000, 0x11223344 }, { 0x08802008, 0x11223344 } }, + { { 0x08802000, 0x08802004 }, { 0x08802004, 0x08802020 }, { 0x08802008, 0x08802020 } }, + { { 0x08802000, 0x08802004 }, { 0x08802004, 0x08802020 }, { 0x08802020, 0x12345678 } }, + { { 0x08802004, 0x22222222 } }, + { { 0x08802004, 0x11111111 } }, + { { 0x08802000, 0x12345678 } }, + { { 0x08802004, 0x11111111 }, { 0x08802008, 0 }, { 0x0880200C, 0x33333333 }, { 0x08802010, 0x44444444 }, { 0x08802014, 0x55 } }, + }; + + CWCheatEngine engine("TEST00000"); + bool success = cheats.size() == 14; + for (size_t i = 0; success && i < expected.size(); i++) { + success = RunCheatTest(engine, cheats[i], expected[i]); + } + + if (success) { + __CtrlUpdateButtons(0, 0xFFFFFFFF); + success = RunCheatTest(engine, cheats[11], { { 0x08802000, 0 }, { 0x08802004, 0x22222222 } }); + success = success && RunCheatTest(engine, cheats[12], { { 0x08802000, 0 } }); + __CtrlUpdateButtons(0x00000200 | 0x00004000, 0); + success = success && RunCheatTest(engine, cheats[11], { { 0x08802000, 0x11111111 }, { 0x08802004, 0 } }); + success = success && RunCheatTest(engine, cheats[12], { { 0x08802000, 0x33333333 } }); + __CtrlUpdateButtons(0, 0xFFFFFFFF); + success = success && RunCheatTest(engine, cheats[13], { { 0x08802000, 0x11111111 } }); + } + + File::Delete(filename); + return success; +} + +static bool TestCheatParseErrors() { + const Path filename("unittest_cheats_bad.ini"); + const char *content = "_S TEST00000\n_C1 Mixed\n_L 0x20002000 0x00000001\n_M 0x08802004 0x00000002\n_C1 Extended\n_N 0xD4000001 0x00000001\n"; + if (!File::WriteStringToFile(true, content, filename)) { + return false; + } + CheatFileParser parser(filename, "TEST00000"); + EXPECT_FALSE(parser.Parse()); + EXPECT_EQ_INT((int)parser.GetErrors().size(), 2); + EXPECT_EQ_INT((int)parser.GetCheats().size(), 1); + EXPECT_EQ_INT((int)parser.GetCheats()[0].lines.size(), 1); + EXPECT_TRUE(parser.GetCheats()[0].format == CheatCodeFormat::CwCheat); + File::Delete(filename); + return true; +} + +bool TestCwCheat() { + Memory::g_MemorySize = Memory::RAM_NORMAL_SIZE; + if (!Memory::Init(Memory::MemMapSetupFlags::Default)) { + printf("Memory::Init failed\n"); + return false; + } + const bool success = RunCheatTests() && TestCheatParseErrors(); + Memory::Shutdown(); + return success; +} diff --git a/unittest/UnitTest.cpp b/unittest/UnitTest.cpp index 2e3de910cb..8b3dd72205 100644 --- a/unittest/UnitTest.cpp +++ b/unittest/UnitTest.cpp @@ -3126,6 +3126,7 @@ bool TestThreadManager(); bool TestVFS(); bool TestZipSlip(); bool TestLzrc(); +bool TestCwCheat(); bool TestMpegCsc(); bool TestSplineTessellation(); bool TestGEMath(); @@ -3514,6 +3515,7 @@ TestItem availableTests[] = { TEST_ITEM(CmdLine), TEST_ITEM(ZipSlip), TEST_ITEM(Lzrc), + TEST_ITEM(CwCheat), TEST_ITEM(MpegCsc), TEST_ITEM(SplineTessellation), TEST_ITEM(GEMath), diff --git a/unittest/UnitTests.vcxproj b/unittest/UnitTests.vcxproj index 77dde06d33..e8efa4eb2a 100644 --- a/unittest/UnitTests.vcxproj +++ b/unittest/UnitTests.vcxproj @@ -295,6 +295,7 @@ + diff --git a/unittest/UnitTests.vcxproj.filters b/unittest/UnitTests.vcxproj.filters index a8d529d4e8..5db81b7bbb 100644 --- a/unittest/UnitTests.vcxproj.filters +++ b/unittest/UnitTests.vcxproj.filters @@ -21,6 +21,7 @@ + From 7b5c263fd6c5435a7ae0e2ddf8adbe771271978f Mon Sep 17 00:00:00 2001 From: LunaMoo Date: Fri, 16 Oct 2026 13:44:27 +0200 Subject: [PATCH 2/2] Cheats unittest: Set currentMIPS before running Fixes crashes when TestCwCheat runs together with other tests that leave currentMIPS pointing elsewhere. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Q5Aw8awynoFTxQmZj3q84E --- unittest/TestCwCheat.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/unittest/TestCwCheat.cpp b/unittest/TestCwCheat.cpp index 917cd0bd55..83eb4a205a 100644 --- a/unittest/TestCwCheat.cpp +++ b/unittest/TestCwCheat.cpp @@ -25,6 +25,7 @@ #include "Core/MemMap.h" #include "Core/MemMapHelpers.h" #include "Core/HLE/sceCtrl.h" +#include "Core/MIPS/MIPS.h" #include "UnitTest.h" // Most of these are the PSPAR tests from TempAR's docs/tests/tests.db, with their expected results. @@ -200,6 +201,8 @@ static bool TestCheatParseErrors() { } bool TestCwCheat() { + // Other tests may leave currentMIPS pointing elsewhere; the cheat engine needs the real one. + currentMIPS = &mipsr4k; Memory::g_MemorySize = Memory::RAM_NORMAL_SIZE; if (!Memory::Init(Memory::MemMapSetupFlags::Default)) { printf("Memory::Init failed\n");