Files
ppsspp/Core/CwCheat.cpp
T
LunaMooandClaude Opus 5.5 3e175060dd 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 <noreply@anthropic.com>
2026-10-15 18:27:33 +02:00

1509 lines
43 KiB
C++

#include <algorithm>
#include <cctype>
#include <cstdint>
#include <cstdio>
#include "Common/Data/Text/I18n.h"
#include "Common/LogReporting.h"
#include "Common/StringUtils.h"
#include "Common/Serialize/Serializer.h"
#include "Common/Serialize/SerializeFuncs.h"
#include "Common/System/OSD.h"
#include "Common/File/FileUtil.h"
#include "Core/CoreTiming.h"
#include "Core/CoreParameter.h"
#include "Core/CwCheat.h"
#include "Core/Config.h"
#include "Core/MemMapHelpers.h"
#include "Core/MIPS/MIPS.h"
#include "Core/ELF/ParamSFO.h"
#include "Core/System.h"
#include "Core/HLE/sceCtrl.h"
#include "Core/MIPS/JitCommon/JitCommon.h"
#include "Core/RetroAchievements.h"
#include "GPU/Common/PostShader.h"
#ifdef _WIN32
#include "Common/Data/Encoding/Utf8.h"
#endif
// Cache invalidation
//
// Writes invalidate the instruction cache (effectively, in PPSSPP's case, the JIT translation cache),
// so code a cheat patches gets recompiled. Reads don't need to: PSP memory never holds JIT data.
static int CheatEvent = -1;
static CWCheatEngine *cheatEngine;
static bool cheatsEnabled;
using namespace SceCtrl;
void hleCheat(u64 userdata, int cyclesLate);
static inline std::string TrimString(std::string_view s) {
auto wsfront = std::find_if_not(s.begin(), s.end(), [](int c) {
// isspace() expects 0 - 255, so convert any sign-extended value.
return std::isspace((u8)c);
});
auto wsback = std::find_if_not(s.rbegin(), s.rend(), [](int c){
return std::isspace((u8)c);
}).base();
return wsback > wsfront ? std::string(wsfront, wsback) : std::string();
}
CheatFileParser::CheatFileParser(const Path &filename, std::string_view gameID) {
fp_ = File::OpenCFile(filename, "rt");
validGameID_ = ReplaceAll(gameID, "-", "");
}
CheatFileParser::~CheatFileParser() {
if (fp_)
fclose(fp_);
}
bool CheatFileParser::Parse() {
// Ugh, using a member variable as loop counter is bad.
for (int lineNumber = 1; fp_ && !feof(fp_); ++lineNumber) {
char temp[2048];
char *tempLine = fgets(temp, sizeof(temp), fp_);
if (!tempLine)
continue;
// Detect UTF-8 BOM sequence, and ignore it.
if (lineNumber == 1 && memcmp(tempLine, "\xEF\xBB\xBF", 3) == 0)
tempLine += 3;
std::string line = TrimString(tempLine);
// Minimum length 5 is shortest possible _ lines name of the game "_G N+"
// and a minimum of 1 displayable character in cheat name string "_C0 1"
// which both equal to 5 characters.
if (line.length() >= 5 && line[0] == '_') {
ParseLine(line, lineNumber);
} else if (line.length() >= 2 && line[0] == '/' && line[1] == '/') {
// Comment, ignore.
} else if (line.length() >= 1 && line[0] == '#') {
// Comment, ignore.
} else if (line.length() > 0) {
errors_.push_back(StringFromFormat("Unrecognized content on line %d: expecting _", lineNumber));
}
}
Flush();
return errors_.empty();
}
void CheatFileParser::Flush() {
if (!pendingLines_.empty()) {
cheats_.push_back(CheatCode{lastCheatInfo_.name, pendingLines_, pendingFormat_});
FlushCheatInfo();
pendingLines_.clear();
}
}
void CheatFileParser::FlushCheatInfo() {
if (lastCheatInfo_.lineNum != 0) {
cheatInfo_.push_back(lastCheatInfo_);
lastCheatInfo_ = { 0 };
}
}
void CheatFileParser::AddError(const std::string &err, int lineNumber) {
errors_.push_back(StringFromFormat("Error on line %d: %s", lineNumber, err.c_str()));
}
void CheatFileParser::ParseLine(const std::string &line, int lineNumber) {
switch (line[1]) {
case 'S':
// Disc ID, validate (for multi-disc cheat files)?
Flush();
++games_;
if (ValidateGameID(line.substr(2))) {
if (gameRiskyEnabled_) {
// We found the right one, so let's not use this risky stuff.
cheats_.clear();
cheatInfo_.clear();
gameRiskyEnabled_ = false;
}
gameEnabled_ = true;
} else if (games_ == 1) {
// Old behavior was to ignore.
// For BC, let's allow if the game id doesn't match, but there's only one line.
gameRiskyEnabled_ = true;
gameEnabled_ = true;
} else {
if (gameRiskyEnabled_) {
// There are multiple games here, kill the risky stuff.
cheats_.clear();
cheatInfo_.clear();
gameRiskyEnabled_ = false;
}
gameEnabled_ = false;
}
return;
case 'G':
// Game title.
return;
case 'C':
Flush();
// Cheat name and activation status.
if (line.length() >= 3 && line[2] >= '1' && line[2] <= '9') {
lastCheatInfo_ = { lineNumber, line.length() >= 5 ? line.substr(4) : "", true };
cheatEnabled_ = true;
} else if (line.length() >= 3 && line[2] == '0') {
lastCheatInfo_ = { lineNumber, line.length() >= 5 ? line.substr(4) : "", false };
cheatEnabled_ = false;
} else {
AddError("could not parse cheat name line", lineNumber);
cheatEnabled_ = false;
return;
}
return;
case 'L':
// CwCheat data line.
ParseDataLine(line.substr(2), lineNumber, CheatCodeFormat::CwCheat);
return;
case 'M':
// 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:
AddError("unknown line type", lineNumber);
return;
}
}
void CheatFileParser::ParseDataLine(const std::string &line, int lineNumber, CheatCodeFormat format) {
if (!gameEnabled_) {
return;
}
if (!cheatEnabled_) {
FlushCheatInfo();
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) {
// 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);
} else {
AddError("expecting two values", lineNumber);
}
}
bool CheatFileParser::ValidateGameID(std::string_view gameID) {
return validGameID_.empty() || ReplaceAll(TrimString(gameID), "-", "") == validGameID_;
}
static void __CheatStop() {
if (cheatEngine) {
delete cheatEngine;
cheatEngine = nullptr;
}
cheatsEnabled = false;
}
static void __CheatStart() {
__CheatStop();
cheatEngine = new CWCheatEngine(g_paramSFO.GetDiscID());
// This only generates ini files on boot, let's leave homebrew ini file for UI.
std::string realGameID = g_paramSFO.GetValueString("DISC_ID");
if (!realGameID.empty()) {
cheatEngine->CreateCheatFile();
}
cheatEngine->ParseCheats();
g_Config.bReloadCheats = false;
cheatsEnabled = true;
}
static int GetRefreshMs() {
int refresh = g_Config.iCwCheatRefreshIntervalMs;
if (!cheatsEnabled) {
refresh = 1000;
}
return refresh;
}
void __CheatInit() {
// Always register the event, want savestates to be compatible whether cheats on or off.
CheatEvent = CoreTiming::RegisterEvent("CheatEvent", &hleCheat);
if (g_Config.bEnableCheats) {
__CheatStart();
}
// Only check once a second for cheats to be enabled.
CoreTiming::ScheduleEvent(msToCycles(GetRefreshMs()), CheatEvent, 0);
}
void __CheatShutdown() {
__CheatStop();
}
void __CheatDoState(PointerWrap &p) {
auto s = p.Section("CwCheat", 0, 2);
if (!s) {
// A savestate from before this section existed. Registering the event type isn't enough -
// CoreTiming::DoState has already replaced the queue with the state's, which has no cheat
// event in it, so without scheduling one here hleCheat never runs again and cheats (plus
// the enable/disable polling) stay dead for the rest of the session.
// (Not when a save failed earlier and every section after comes back missing.)
if (p.mode == PointerWrap::MODE_READ) {
CheatEvent = -1;
CoreTiming::RestoreRegisterEvent(CheatEvent, "CheatEvent", &hleCheat);
CoreTiming::RemoveEvent(CheatEvent);
CoreTiming::ScheduleEvent(msToCycles(GetRefreshMs()), CheatEvent, 0);
}
return;
}
Do(p, CheatEvent);
CoreTiming::RestoreRegisterEvent(CheatEvent, "CheatEvent", &hleCheat);
if (s < 2) {
// Before this we didn't have a checkpoint, so reset didn't work.
// Let's just force one in.
CoreTiming::RemoveEvent(CheatEvent);
CoreTiming::ScheduleEvent(msToCycles(GetRefreshMs()), CheatEvent, 0);
}
}
void hleCheat(u64 userdata, int cyclesLate) {
bool shouldBeEnabled = !Achievements::HardcoreModeActive() && g_Config.bEnableCheats;
if (cheatsEnabled != shouldBeEnabled) {
// Okay, let's move to the desired state, then.
if (shouldBeEnabled) {
__CheatStart();
} else {
__CheatStop();
}
}
// Check periodically for cheats.
CoreTiming::ScheduleEvent(msToCycles(GetRefreshMs()), CheatEvent, 0);
if (!cheatEngine || !cheatsEnabled) {
return;
}
if (g_Config.bReloadCheats) { // Checks if the "reload cheats" button has been pressed.
cheatEngine->ParseCheats();
g_Config.bReloadCheats = false;
}
cheatEngine->Run();
}
CWCheatEngine::CWCheatEngine(std::string_view gameID) : gameID_(gameID) {
filename_ = GetSysDirectory(DIRECTORY_CHEATS) / (gameID_ + ".ini");
}
void CWCheatEngine::CreateCheatFile() {
File::CreateFullPath(GetSysDirectory(DIRECTORY_CHEATS));
if (!File::Exists(filename_)) {
FILE *f = File::OpenCFile(filename_, "wb");
if (f) {
fwrite("\xEF\xBB\xBF\n", 1, 4, f);
fclose(f);
}
if (!File::Exists(filename_)) {
auto err = GetI18NCategory(I18NCat::ERRORS);
g_OSD.Show(OSDType::MESSAGE_ERROR, err->T("Unable to create cheat file, disk may be full"));
}
}
}
void CWCheatEngine::ParseCheats() {
CheatFileParser parser(filename_, gameID_);
parser.Parse();
// TODO: Report errors in a user-visible way.
for (auto &error : parser.GetErrors()) {
ERROR_LOG(Log::System, "CwCheat error: %s", error.c_str());
}
cheats_ = parser.GetCheats();
}
std::vector<CheatFileInfo> CWCheatEngine::FileInfo() const {
CheatFileParser parser(filename_, gameID_);
parser.Parse();
return parser.GetFileInfo();
}
void CWCheatEngine::InvalidateICache(u32 addr, int size) const {
// Round start down and size up to the nearest word.
u32 aligned = addr & ~3;
int alignedSize = (addr + size - aligned + 3) & ~3;
currentMIPS->InvalidateICacheRangeDeferred(aligned, alignedSize);
}
enum class CheatOp {
Invalid,
Noop,
Write,
Add,
Subtract,
Or,
And,
Xor,
MultiWrite,
CopyBytesFrom,
Vibration,
VibrationFromMemory,
PostShader,
PostShaderFromMemory,
Delay,
Assert,
IfEqual,
IfNotEqual,
IfLess,
IfGreater,
IfAddrEqual,
IfAddrNotEqual,
IfAddrLess,
IfAddrGreater,
IfPressed,
IfNotPressed,
CwCheatPointerCommands,
};
struct CheatOperation {
CheatOp op;
uint32_t addr;
int sz;
uint32_t val;
union {
struct {
uint32_t count;
uint32_t step;
uint32_t add;
} multiWrite;
struct {
uint32_t destAddr;
} copyBytesFrom;
struct {
uint32_t skip;
} ifTypes;
struct {
uint32_t skip;
uint32_t compareAddr;
} ifAddrTypes;
struct {
int offset;
int baseOffset;
int count;
int type;
} pointerCommands;
struct {
uint16_t vibrL;
uint16_t vibrR;
uint8_t vibrLTime;
uint8_t vibrRTime;
} vibrationValues;
struct {
union {
float f;
uint32_t u;
} value;
uint8_t shader;
uint8_t uniform;
uint8_t format;
} PostShaderUniform;
};
};
CheatOperation CWCheatEngine::InterpretNextCwCheat(const CheatCode &cheat, size_t &i) {
const CheatLine &line1 = cheat.lines[i++];
const uint32_t &arg = line1.part2;
// Filled as needed.
u32 addr;
int type = line1.part1 >> 28;
switch (type) {
case 0x0: // Write 8-bit data (up to 4 bytes.)
addr = GetAddress(line1.part1 & 0x0FFFFFFF);
if (arg & 0xFFFF0000)
return { CheatOp::Write, addr, 4, arg };
else if (arg & 0x0000FF00)
return { CheatOp::Write, addr, 2, arg };
else
return { CheatOp::Write, addr, 1, arg };
case 0x1: // Write 16-bit data.
addr = GetAddress(line1.part1 & 0x0FFFFFFF);
return { CheatOp::Write, addr, 2, arg };
case 0x2: // Write 32-bit data.
addr = GetAddress(line1.part1 & 0x0FFFFFFF);
return { CheatOp::Write, addr, 4, arg };
case 0x3: // Increment/decrement data.
addr = GetAddress(arg & 0x0FFFFFFF);
switch ((line1.part1 >> 20) & 0xF) {
case 1:
return { CheatOp::Add, addr, 1, line1.part1 & 0xFF };
case 2:
return { CheatOp::Subtract, addr, 1, line1.part1 & 0xFF };
case 3:
return { CheatOp::Add, addr, 2, line1.part1 & 0xFFFF };
case 4:
return { CheatOp::Subtract, addr, 2, line1.part1 & 0xFFFF };
case 5:
if (i < cheat.lines.size())
return { CheatOp::Add, addr, 4, cheat.lines[i++].part1 };
return { CheatOp::Invalid };
case 6:
if (i < cheat.lines.size())
return { CheatOp::Subtract, addr, 4, cheat.lines[i++].part1 };
return { CheatOp::Invalid };
default:
return { CheatOp::Invalid };
}
break;
case 0x4: // 32-bit multi-write patch data.
addr = GetAddress(line1.part1 & 0x0FFFFFFF);
if (i < cheat.lines.size()) {
const CheatLine &line2 = cheat.lines[i++];
CheatOperation op = { CheatOp::MultiWrite, addr, 4, line2.part1 };
op.multiWrite.count = arg >> 16;
op.multiWrite.step = (arg & 0xFFFF) * 4;
op.multiWrite.add = line2.part2;
return op;
}
return { CheatOp::Invalid };
case 0x5: // Memcpy command.
addr = GetAddress(line1.part1 & 0x0FFFFFFF);
if (i < cheat.lines.size()) {
const CheatLine &line2 = cheat.lines[i++];
CheatOperation op = { CheatOp::CopyBytesFrom, addr, 0, arg };
op.copyBytesFrom.destAddr = GetAddress(line2.part1 & 0x0FFFFFFF);
return op;
}
return { CheatOp::Invalid };
case 0x6: // Pointer commands.
addr = GetAddress(line1.part1 & 0x0FFFFFFF);
if (i < cheat.lines.size()) {
const CheatLine &line2 = cheat.lines[i++];
int count = (line2.part1 & 0xFFFF) - 1;
// Validate lines to process - make sure we stay inside cheat.lines.
if (i + count > cheat.lines.size())
return { CheatOp::Invalid };
CheatOperation op = { CheatOp::CwCheatPointerCommands, addr, 0, arg };
op.pointerCommands.offset = (int)line2.part2;
// TODO: Verify sign handling. Is this really supposed to sign extend?
op.pointerCommands.baseOffset = ((int)line2.part1 >> 20) * 4;
op.pointerCommands.count = count;
op.pointerCommands.type = (line2.part1 >> 16) & 0xF;
return op;
}
return { CheatOp::Invalid };
case 0x7: // Boolean data operations.
addr = GetAddress(line1.part1 & 0x0FFFFFFF);
switch (arg >> 16) {
case 0x0000: // 8-bit OR.
return { CheatOp::Or, addr, 1, arg & 0xFF };
case 0x0001: // 16-bit OR.
return { CheatOp::Or, addr, 2, arg & 0xFFFF };
case 0x0002: // 8-bit AND.
return { CheatOp::And, addr, 1, arg & 0xFF };
case 0x0003: // 16-bit AND.
return { CheatOp::And, addr, 2, arg & 0xFFFF };
case 0x0004: // 8-bit XOR.
return { CheatOp::Xor, addr, 1, arg & 0xFF };
case 0x0005: // 16-bit XOR.
return { CheatOp::Xor, addr, 2, arg & 0xFFFF };
}
return { CheatOp::Invalid };
case 0x8: // 8-bit or 16-bit multi-write patch data.
addr = GetAddress(line1.part1 & 0x0FFFFFFF);
if (i < cheat.lines.size()) {
const CheatLine &line2 = cheat.lines[i++];
const bool is8Bit = (line2.part1 & 0xFFFF0000) == 0;
const uint32_t val = is8Bit ? (line2.part1 & 0xFF) : (line2.part1 & 0xFFFF);
CheatOperation op = { CheatOp::MultiWrite, addr, is8Bit ? 1 : 2, val };
op.multiWrite.count = arg >> 16;
op.multiWrite.step = (arg & 0xFFFF) * (is8Bit ? 1 : 2);
op.multiWrite.add = line2.part2;
return op;
}
return { CheatOp::Invalid };
case 0xA: // PPSSPP specific cheats
switch (line1.part1 >> 24 & 0xF) {
case 0x0: // 0x0 sets gamepad vibration by cheat parameters
{
CheatOperation op = { CheatOp::Vibration };
op.vibrationValues.vibrL = line1.part1 & 0x0000FFFF;
op.vibrationValues.vibrR = line1.part2 & 0x0000FFFF;
op.vibrationValues.vibrLTime = (line1.part1 >> 16) & 0x000000FF;
op.vibrationValues.vibrRTime = (line1.part2 >> 16) & 0x000000FF;
return op;
}
case 0x1: // 0x1 reads value for gamepad vibration from memory
addr = line1.part2;
return { CheatOp::VibrationFromMemory, addr };
case 0x2: // 0x2 sets postprocessing shader uniform
{
CheatOperation op = { CheatOp::PostShader };
op.PostShaderUniform.uniform = line1.part1 & 0x000000FF;
op.PostShaderUniform.shader = (line1.part1 >> 16) & 0x000000FF;
op.PostShaderUniform.value.u = line1.part2;
return op;
}
case 0x3: // 0x3 sets postprocessing shader uniform from memory
{
addr = line1.part2;
CheatOperation op = { CheatOp::PostShaderFromMemory, addr };
op.PostShaderUniform.uniform = line1.part1 & 0x000000FF;
op.PostShaderUniform.format = (line1.part1 >> 8) & 0x000000FF;
op.PostShaderUniform.shader = (line1.part1 >> 16) & 0x000000FF;
return op;
}
// Place for other PPSSPP specific cheats
default:
return { CheatOp::Invalid };
}
case 0xB: // Delay command.
return { CheatOp::Delay, 0, 0, arg };
case 0xC: // 32-bit equal check / code stopper.
addr = GetAddress(line1.part1 & 0x0FFFFFFF);
return { CheatOp::Assert, addr, 4, arg };
case 0xD: // Line skip tests & joker codes.
switch (arg >> 28) {
case 0x0: // 16-bit next line skip test.
case 0x2: // 8-bit next line skip test.
addr = GetAddress(line1.part1 & 0x0FFFFFFF);
{
const bool is8Bit = (arg >> 28) == 0x2;
const uint32_t val = is8Bit ? (arg & 0xFF) : (arg & 0xFFFF);
CheatOp opcode;
switch ((arg >> 20) & 0xF) {
case 0x0:
opcode = CheatOp::IfEqual;
break;
case 0x1:
opcode = CheatOp::IfNotEqual;
break;
case 0x2:
opcode = CheatOp::IfLess;
break;
case 0x3:
opcode = CheatOp::IfGreater;
break;
default:
return { CheatOp::Invalid };
}
CheatOperation op = { opcode, addr, is8Bit ? 1 : 2, val };
op.ifTypes.skip = 1;
return op;
}
case 0x1: // Joker code - button pressed.
case 0x3: // Inverse joker code - button not pressed.
{
bool pressed = (arg >> 28) == 0x1;
CheatOperation op = { pressed ? CheatOp::IfPressed : CheatOp::IfNotPressed, 0, 0, arg & 0x0FFFFFFF };
op.ifTypes.skip = (line1.part1 & 0xFF) + 1;
return op;
}
case 0x4: // Adress equal test.
case 0x5: // Address not equal test.
case 0x6: // Address less than test.
case 0x7: // Address greater than test.
addr = GetAddress(line1.part1 & 0x0FFFFFFF);
if (i < cheat.lines.size()) {
const CheatLine &line2 = cheat.lines[i++];
const int sz = 1 << (line2.part2 & 0xF);
CheatOp opcode;
switch (arg >> 28) {
case 0x4:
opcode = CheatOp::IfAddrEqual;
break;
case 0x5:
opcode = CheatOp::IfAddrNotEqual;
break;
case 0x6:
opcode = CheatOp::IfAddrLess;
break;
case 0x7:
opcode = CheatOp::IfAddrGreater;
break;
default:
return { CheatOp::Invalid };
}
CheatOperation op = { opcode, addr, sz, 0 };
op.ifAddrTypes.skip = line2.part1;
op.ifAddrTypes.compareAddr = GetAddress(arg & 0x0FFFFFFF);
return op;
}
return { CheatOp::Invalid };
default:
return { CheatOp::Invalid };
}
case 0xE: // Multiple line skip tests.
addr = GetAddress(arg & 0x0FFFFFFF);
{
const bool is8Bit = (line1.part1 >> 24) == 0xE1;
const uint32_t val = is8Bit ? (line1.part1 & 0xFF) : (line1.part1 & 0xFFFF);
CheatOp opcode;
switch (arg >> 28) {
case 0x0:
opcode = CheatOp::IfEqual;
break;
case 0x1:
opcode = CheatOp::IfNotEqual;
break;
case 0x2:
opcode = CheatOp::IfLess;
break;
case 0x3:
opcode = CheatOp::IfGreater;
break;
default:
return { CheatOp::Invalid };
}
CheatOperation op = { opcode, addr, is8Bit ? 1 : 2, val };
op.ifTypes.skip = (line1.part1 >> 16) & (is8Bit ? 0xFF : 0xFFF);
return op;
}
default:
return { CheatOp::Invalid };
}
}
void CWCheatEngine::ApplyMemoryOperator(const CheatOperation &op, uint32_t(*oper)(uint32_t, uint32_t)) {
if (Memory::IsValidRange(op.addr, op.sz)) {
InvalidateICache(op.addr, op.sz);
if (op.sz == 1)
Memory::WriteUnchecked_U8((u8)oper(Memory::ReadUnchecked_U8(op.addr), op.val), op.addr);
else if (op.sz == 2)
Memory::WriteUnchecked_U16((u16)oper(Memory::ReadUnchecked_U16(op.addr), op.val),op. addr);
else if (op.sz == 4)
Memory::WriteUnchecked_U32((u32)oper(Memory::ReadUnchecked_U32(op.addr), op.val), op.addr);
} else {
// Report memory error
}
}
bool CWCheatEngine::TestIf(const CheatOperation &op, bool(*oper)(int, int)) const {
if (Memory::IsValidRange(op.addr, op.sz)) {
int memoryValue = 0;
if (op.sz == 1)
memoryValue = (int)Memory::ReadUnchecked_U8(op.addr);
else if (op.sz == 2)
memoryValue = (int)Memory::ReadUnchecked_U16(op.addr);
else if (op.sz == 4)
memoryValue = (int)Memory::ReadUnchecked_U32(op.addr);
return oper(memoryValue, (int)op.val);
}
return false;
}
bool CWCheatEngine::TestIfAddr(const CheatOperation &op, bool(*oper)(int, int)) const {
if (Memory::IsValidRange(op.addr, op.sz) && Memory::IsValidRange(op.ifAddrTypes.compareAddr, op.sz)) {
int memoryValue1 = 0;
int memoryValue2 = 0;
if (op.sz == 1) {
memoryValue1 = (int)Memory::ReadUnchecked_U8(op.addr);
memoryValue2 = (int)Memory::ReadUnchecked_U8(op.ifAddrTypes.compareAddr);
} else if (op.sz == 2) {
memoryValue1 = (int)Memory::ReadUnchecked_U16(op.addr);
memoryValue2 = (int)Memory::ReadUnchecked_U16(op.ifAddrTypes.compareAddr);
} else if (op.sz == 4) {
memoryValue1 = (int)Memory::ReadUnchecked_U32(op.addr);
memoryValue2 = (int)Memory::ReadUnchecked_U32(op.ifAddrTypes.compareAddr);
}
return oper(memoryValue1, memoryValue2);
}
return false;
}
void CWCheatEngine::ExecuteOp(const CheatOperation &op, const CheatCode &cheat, size_t &i) {
switch (op.op) {
case CheatOp::Invalid:
i = cheat.lines.size();
break;
case CheatOp::Noop:
break;
case CheatOp::Write:
if (Memory::IsValidRange(op.addr, op.sz)) {
InvalidateICache(op.addr, op.sz);
if (op.sz == 1)
Memory::WriteUnchecked_U8((u8)op.val, op.addr);
else if (op.sz == 2)
Memory::WriteUnchecked_U16((u16)op.val, op.addr);
else if (op.sz == 4)
Memory::WriteUnchecked_U32((u32)op.val, op.addr);
}
break;
case CheatOp::Add:
ApplyMemoryOperator(op, [](uint32_t a, uint32_t b) {
return a + b;
});
break;
case CheatOp::Subtract:
ApplyMemoryOperator(op, [](uint32_t a, uint32_t b) {
return a - b;
});
break;
case CheatOp::Or:
ApplyMemoryOperator(op, [](uint32_t a, uint32_t b) {
return a | b;
});
break;
case CheatOp::And:
ApplyMemoryOperator(op, [](uint32_t a, uint32_t b) {
return a & b;
});
break;
case CheatOp::Xor:
ApplyMemoryOperator(op, [](uint32_t a, uint32_t b) {
return a ^ b;
});
break;
case CheatOp::MultiWrite:
{
u64 range = (u64)op.multiWrite.count * (u64)op.multiWrite.step + op.sz;
if (range < 24 * 1024 * 1024 && Memory::IsValidRange(op.addr, op.multiWrite.count * op.multiWrite.step + op.sz)) {
InvalidateICache(op.addr, op.multiWrite.count * op.multiWrite.step + op.sz);
uint32_t data = op.val;
uint32_t addr = op.addr;
// TODO: Can coalesce the range check.
for (uint32_t a = 0; a < op.multiWrite.count; a++) {
if (Memory::IsValidRange(addr, op.sz)) {
if (op.sz == 1)
Memory::WriteUnchecked_U8((u8)data, addr);
else if (op.sz == 2)
Memory::WriteUnchecked_U16((u16)data, addr);
else if (op.sz == 4)
Memory::WriteUnchecked_U32((u32)data, addr);
}
addr += op.multiWrite.step;
data += op.multiWrite.add;
}
}
break;
}
case CheatOp::CopyBytesFrom:
if (Memory::IsValidRange(op.addr, op.val) && Memory::IsValidRange(op.copyBytesFrom.destAddr, op.val)) {
InvalidateICache(op.copyBytesFrom.destAddr, op.val);
Memory::Memcpy(op.copyBytesFrom.destAddr, op.addr, op.val, "CwCheat");
}
break;
case CheatOp::Vibration:
if (op.vibrationValues.vibrL > 0) {
SetLeftVibration(op.vibrationValues.vibrL);
SetVibrationLeftDropout(op.vibrationValues.vibrLTime);
}
if (op.vibrationValues.vibrR > 0) {
SetRightVibration(op.vibrationValues.vibrR);
SetVibrationRightDropout(op.vibrationValues.vibrRTime);
}
break;
case CheatOp::VibrationFromMemory:
if (Memory::IsValidRange(op.addr, 8)) {
uint16_t checkLeftVibration = Memory::ReadUnchecked_U16(op.addr);
uint16_t checkRightVibration = Memory::ReadUnchecked_U16(op.addr + 0x2);
if (checkLeftVibration > 0) {
SetLeftVibration(checkLeftVibration);
SetVibrationLeftDropout(Memory::ReadUnchecked_U8(op.addr + 0x4));
}
if (checkRightVibration > 0) {
SetRightVibration(checkRightVibration);
SetVibrationRightDropout(Memory::ReadUnchecked_U8(op.addr + 0x6));
}
}
break;
case CheatOp::PostShader:
{
auto shaderChain = GetFullPostShadersChain(g_Config.vPostShaderNames);
if (op.PostShaderUniform.shader < shaderChain.size()) {
std::string shaderName = shaderChain[op.PostShaderUniform.shader]->section;
g_Config.mPostShaderSetting[StringFromFormat("%sSettingCurrentValue%d", shaderName.c_str(), op.PostShaderUniform.uniform + 1)] = op.PostShaderUniform.value.f;
}
}
break;
case CheatOp::PostShaderFromMemory:
{
auto shaderChain = GetFullPostShadersChain(g_Config.vPostShaderNames);
if (Memory::IsValidRange(op.addr, 4) && op.PostShaderUniform.shader < shaderChain.size()) {
union {
float f;
uint32_t u;
} value;
value.u = Memory::ReadUnchecked_U32(op.addr); // we check the range above
std::string shaderName = shaderChain[op.PostShaderUniform.shader]->section;
switch (op.PostShaderUniform.format) {
case 0:
g_Config.mPostShaderSetting[StringFromFormat("%sSettingCurrentValue%d", shaderName.c_str(), op.PostShaderUniform.uniform + 1)] = value.u & 0x000000FF;
break;
case 1:
g_Config.mPostShaderSetting[StringFromFormat("%sSettingCurrentValue%d", shaderName.c_str(), op.PostShaderUniform.uniform + 1)] = value.u & 0x0000FFFF;
break;
case 2:
g_Config.mPostShaderSetting[StringFromFormat("%sSettingCurrentValue%d", shaderName.c_str(), op.PostShaderUniform.uniform + 1)] = value.u;
break;
case 3:
g_Config.mPostShaderSetting[StringFromFormat("%sSettingCurrentValue%d", shaderName.c_str(), op.PostShaderUniform.uniform + 1)] = value.f;
break;
}
}
}
break;
case CheatOp::Delay:
// TODO: Not supported.
break;
case CheatOp::Assert:
if (Memory::IsValidRange(op.addr, 4)) {
if (Memory::ReadUnchecked_U32(op.addr) != op.val) {
i = cheat.lines.size();
}
}
break;
case CheatOp::IfEqual:
if (!TestIf(op, [](int a, int b) { return a == b; })) {
i += (size_t)op.ifTypes.skip;
}
break;
case CheatOp::IfNotEqual:
if (!TestIf(op, [](int a, int b) { return a != b; })) {
i += (size_t)op.ifTypes.skip;
}
break;
case CheatOp::IfLess:
if (!TestIf(op, [](int a, int b) { return a < b; })) {
i += (size_t)op.ifTypes.skip;
}
break;
case CheatOp::IfGreater:
if (!TestIf(op, [](int a, int b) { return a > b; })) {
i += (size_t)op.ifTypes.skip;
}
break;
case CheatOp::IfAddrEqual:
if (!TestIfAddr(op, [](int a, int b) { return a == b; })) {
i += (size_t)op.ifAddrTypes.skip;
}
break;
case CheatOp::IfAddrNotEqual:
if (!TestIfAddr(op, [](int a, int b) { return a != b; })) {
i += (size_t)op.ifAddrTypes.skip;
}
break;
case CheatOp::IfAddrLess:
if (!TestIfAddr(op, [](int a, int b) { return a < b; })) {
i += (size_t)op.ifAddrTypes.skip;
}
break;
case CheatOp::IfAddrGreater:
if (!TestIfAddr(op, [](int a, int b) { return a > b; })) {
i += (size_t)op.ifAddrTypes.skip;
}
break;
case CheatOp::IfPressed:
// Button Code
// SELECT 0x00000001
// START 0x00000008
// DPAD UP 0x00000010
// DPAD RIGHT 0x00000020
// DPAD DOWN 0x00000040
// DPAD LEFT 0x00000080
// L TRIGGER 0x00000100
// R TRIGGER 0x00000200
// TRIANGLE 0x00001000
// CIRCLE 0x00002000
// CROSS 0x00004000
// SQUARE 0x00008000
// HOME 0x00010000
// HOLD 0x00020000
// WLAN 0x00040000
// REMOTE HOLD 0x00080000
// VOLUME UP 0x00100000
// VOLUME DOWN 0x00200000
// SCREEN 0x00400000
// NOTE 0x00800000
if ((__CtrlPeekButtons() & op.val) != op.val) {
i += (size_t)op.ifTypes.skip;
}
break;
case CheatOp::IfNotPressed:
if ((__CtrlPeekButtons() & op.val) == op.val) {
i += (size_t)op.ifTypes.skip;
}
break;
case CheatOp::CwCheatPointerCommands:
{
if (!Memory::IsValidRange(op.addr + op.pointerCommands.baseOffset, 4)) {
break;
}
u32 base = Memory::ReadUnchecked_U32(op.addr + op.pointerCommands.baseOffset);
u32 val = op.val;
int type = op.pointerCommands.type;
for (int a = 0; a < op.pointerCommands.count; ++a) {
const CheatLine &line = cheat.lines[i++];
switch (line.part1 >> 28) {
case 0x1: // type copy byte
if (Memory::IsValidRange(op.addr, 4) && Memory::IsValidRange(op.addr + op.pointerCommands.baseOffset, 4)) {
u32 srcAddr = Memory::ReadUnchecked_U32(op.addr) + op.pointerCommands.offset;
u32 dstAddr = Memory::ReadUnchecked_U32(op.addr + op.pointerCommands.baseOffset) + (line.part1 & 0x0FFFFFFF);
if (Memory::IsValidRange(dstAddr, val) && Memory::IsValidRange(srcAddr, val)) {
InvalidateICache(dstAddr, val);
Memory::Memcpy(dstAddr, srcAddr, val, "CwCheat");
}
// Don't perform any further action.
type = -1;
}
break;
case 0x2:
case 0x3: // type pointer walk
{
int walkOffset = (int)line.part1 & 0x0FFFFFFF;
if ((line.part1 >> 28) == 0x3) {
walkOffset = -walkOffset;
}
if (Memory::IsValidRange(base + walkOffset, 4)) {
// TODO: I've seen crashes here. Presumably an unaligned pointer just off the edge of memory.
// We should probably check pointer validity and invalidate the cheat if this happens.
base = Memory::ReadUnchecked_U32(base + walkOffset);
switch (line.part2 >> 28) {
case 0x2:
case 0x3: // type pointer walk
walkOffset = line.part2 & 0x0FFFFFFF;
if ((line.part2 >> 28) == 0x3) {
walkOffset = -walkOffset;
}
if (Memory::IsValidRange(base + walkOffset, 4)) {
base = Memory::ReadUnchecked_U32(base + walkOffset);
}
break;
default:
// Unexpected value in cheat line?
break;
}
}
}
break;
case 0x9: // type multi address write
base += line.part1 & 0x0FFFFFFF;
val += line.part2;
break;
default:
// Unexpected value in cheat line?
break;
}
}
switch (type) {
case 0: // 8 bit write
if (Memory::IsValidAddress(base + op.pointerCommands.offset)) {
InvalidateICache(base + op.pointerCommands.offset, 1);
Memory::WriteUnchecked_U8((u8)val, base + op.pointerCommands.offset);
}
break;
case 1: // 16-bit write
if (Memory::IsValidRange(base + op.pointerCommands.offset, 2)) {
InvalidateICache(base + op.pointerCommands.offset, 2);
Memory::WriteUnchecked_U16((u16)val, base + op.pointerCommands.offset);
}
break;
case 2: // 32-bit write
if (Memory::IsValidRange(base + op.pointerCommands.offset, 4)) {
InvalidateICache(base + op.pointerCommands.offset, 4);
Memory::WriteUnchecked_U32((u32)val, base + op.pointerCommands.offset);
}
break;
case 3: // 8 bit inverse write
if (Memory::IsValidAddress(base - op.pointerCommands.offset)) {
InvalidateICache(base - op.pointerCommands.offset, 1);
Memory::WriteUnchecked_U8((u8)val, base - op.pointerCommands.offset);
}
break;
case 4: // 16-bit inverse write
if (Memory::IsValidRange(base - op.pointerCommands.offset, 2)) {
InvalidateICache(base - op.pointerCommands.offset, 2);
Memory::WriteUnchecked_U16((u16)val, base - op.pointerCommands.offset);
}
break;
case 5: // 32-bit inverse write
if (Memory::IsValidRange(base - op.pointerCommands.offset, 4)) {
InvalidateICache(base - op.pointerCommands.offset, 4);
Memory::WriteUnchecked_U32((u32)val, base - op.pointerCommands.offset);
}
break;
case -1: // Operation already performed, nothing to do
break;
}
}
break;
default:
_assert_(false);
}
}
// 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 (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);
ExecuteOp(op, cheat, i);
}
}
}
bool CWCheatEngine::HasCheats() {
return !cheats_.empty();
}
bool CheatsInEffect() {
if (!cheatEngine || !cheatsEnabled || Achievements::HardcoreModeActive())
return false;
return cheatEngine->HasCheats();
}
bool DetectCheatTitle(std::string_view name, std::string_view *title) {
// Use Saramagrean title convention (the most common cheats.db file).
const size_t firstOffset = name.find("_[>>>");
const size_t secondOffset = name.rfind("<<<]_");
const bool isTitle = firstOffset != std::string::npos && secondOffset != std::string::npos && firstOffset < secondOffset;
if (!isTitle) {
return false;
}
// Extract the title substring.
*title = std::string_view(name.data() + firstOffset + 5, secondOffset - (firstOffset + 5));
return true;
}
bool DetectCheatPostComment(std::string_view name, std::string_view *comment) {
const size_t firstOffset = name.find("\xE2\x86\x91[#"); // Up arrow and [#
const size_t secondOffset = name.rfind(']');
const bool isPostComment = firstOffset != std::string::npos && secondOffset != std::string::npos && secondOffset > firstOffset;
if (!isPostComment) {
return false;
}
*comment = std::string_view(name.data() + firstOffset + 5, secondOffset - (firstOffset + 5));
return true;
}