mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-09-03 11:15:20 +02:00
System.cpp stamped BootState::Complete unconditionally after InitGPU(), overwriting the Failed that InitGPU sets when GPU_Init() fails - after it has already run CPU_Shutdown(). PSP_InitUpdate then took the success path on a core that no longer existed, down to a null Memory::base, and the first guest access dereferenced it. InitGPU now reports failure and both callers honor it. (The libretro path had the same problem from the other direction: it calls InitGPU after the Failed check.) HandleAssert called g_assertCancelCallback directly on the IDCANCEL path, without the null check its own BreakIntoPSPDebugger() helper does - and EmuScreen clears the callback when a game is unloaded. So any assert after returning to the menu turned "Cancel: skip and break into PPSSPP debugger" into a null jump, from the one button whose entire purpose is surviving the assert. __CheatDoState registered the cheat event type when the savestate had no CwCheat section, but never scheduled it. CoreTiming::DoState has already swapped in the state's event queue by then, which doesn't contain one either - so loading an old savestate silently killed cheats, and the enable/disable polling with them, for the rest of the session. Achievements::ChangeUMD set g_isIdentifying and returned without clearing it when hashing failed, leaving IsBlockingExecution() true forever - EmuScreen stops running the CPU and the game is frozen until restart. Reachable from a disc swap on any ISO whose PARAM.SFO or EBOOT.BIN can't be read. x64Analyzer routed opcode 0x88 into the write path but had no case for it, so it hit the default, logged from inside the crash handler, and failed. 0x88 is exactly what the x64 JIT emits for a guest sb, so MemFault could never skip or ignore a bad byte store the way it can a word one. Handle the 8-bit forms, and drop the 0x8a/0x8b cases in the read path that the same 0xF0 mask made unreachable. Covered by a new CheckAnalyze case, which fails without this change. 314 pspautotests pass, all unit tests pass.
1213 lines
35 KiB
C++
1213 lines
35 KiB
C++
#include <algorithm>
|
|
#include <cctype>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
|
|
#include "Common/Data/Text/I18n.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
|
|
//
|
|
// It should be obvious why we need to invalidate the instruction cache (effectively, in PPSSPP's case,
|
|
// the JIT translation cache) for writes. But currently we do also need to do it for reads, in case
|
|
// cheats check what MIPS opcode is at an address - the JIT sometimes overwrites them. Invalidating
|
|
// the cache will restore that.
|
|
//
|
|
// Long term, we should get rid of the instruction overwriting hack, or we could use Memory::Read_Instruction
|
|
// with workarounds for 8 and 16 bit reads.
|
|
|
|
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_});
|
|
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);
|
|
return;
|
|
|
|
case 'M':
|
|
// TempAR data line.
|
|
AddError("TempAR codes not supported", lineNumber);
|
|
return;
|
|
|
|
default:
|
|
AddError("unknown line type", lineNumber);
|
|
return;
|
|
}
|
|
}
|
|
|
|
void CheatFileParser::ParseDataLine(const std::string &line, int lineNumber) {
|
|
if (!gameEnabled_) {
|
|
return;
|
|
}
|
|
|
|
if (!cheatEnabled_) {
|
|
FlushCheatInfo();
|
|
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()) {
|
|
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;
|
|
|
|
// Horrible hack for Tony Hawk - Underground 2. See #3854. Avoids crashing somehow
|
|
// but still causes regular JIT invalidations which causes stutters.
|
|
if (PSP_CoreParameter().compat.flags().JitInvalidationHack) {
|
|
refresh = 2;
|
|
}
|
|
|
|
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.
|
|
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 (PSP_CoreParameter().compat.flags().JitInvalidationHack) {
|
|
std::string gameTitle = g_paramSFO.GetValueString("DISC_ID");
|
|
|
|
// Horrible hack for Tony Hawk - Underground 2. See #3854. Avoids crashing somehow
|
|
// but still causes regular JIT invalidations which causes stutters.
|
|
if (gameTitle == "ULUS10014") {
|
|
currentMIPS->InvalidateICacheRangeDeferred(0x08865600, 72);
|
|
currentMIPS->InvalidateICacheRangeDeferred(0x08865690, 4);
|
|
} else if (gameTitle == "ULES00033" || gameTitle == "ULES00034" || gameTitle == "ULES00035") { // euro, also 34 and 35
|
|
currentMIPS->InvalidateICacheRangeDeferred(0x088655D8, 72);
|
|
currentMIPS->InvalidateICacheRangeDeferred(0x08865668, 4);
|
|
} else if (gameTitle == "ULUS10138") { // MTX MotoTrax US
|
|
currentMIPS->InvalidateICacheRangeDeferred(0x0886DCC0, 72);
|
|
currentMIPS->InvalidateICacheRangeDeferred(0x0886DC20, 4);
|
|
currentMIPS->InvalidateICacheRangeDeferred(0x0886DD40, 4);
|
|
} else if (gameTitle == "ULES00581") { // MTX MotoTrax EU (ported from US cwcheat codes)
|
|
currentMIPS->InvalidateICacheRangeDeferred(0x0886E1D8, 72);
|
|
currentMIPS->InvalidateICacheRangeDeferred(0x0886E138, 4);
|
|
currentMIPS->InvalidateICacheRangeDeferred(0x0886E258, 4);
|
|
}
|
|
}
|
|
|
|
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)) {
|
|
InvalidateICache(op.addr, op.sz); // See note at top of file
|
|
|
|
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)) {
|
|
InvalidateICache(op.addr, op.sz); // See note at top of file
|
|
InvalidateICache(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.addr, op.val); // See note at top of file
|
|
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)) {
|
|
InvalidateICache(op.addr, 4); // See note at top of file
|
|
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;
|
|
}
|
|
InvalidateICache(op.addr + op.pointerCommands.baseOffset, 4); // See note at top of file
|
|
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)) {
|
|
InvalidateICache(op.addr, 4); // See note at top of file
|
|
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);
|
|
InvalidateICache(srcAddr, val); // See note at top of file
|
|
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)) {
|
|
InvalidateICache(base + walkOffset, 4); // See note at top of file
|
|
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);
|
|
}
|
|
}
|
|
|
|
void CWCheatEngine::Run() {
|
|
if (Achievements::HardcoreModeActive()) {
|
|
return;
|
|
}
|
|
|
|
for (const CheatCode &cheat : cheats_) {
|
|
// 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;
|
|
}
|