#include "cheats.h" #include "common/assert.h" #include "common/file_system.h" #include "common/log.h" #include "common/string.h" #include "common/string_util.h" #include "cpu_core.h" #include "host_interface.h" #include #include #include Log_SetChannel(Cheats); using KeyValuePairVector = std::vector>; CheatList::CheatList() = default; CheatList::~CheatList() = default; static bool IsHexCharacter(char c) { return (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f') || (c >= '0' && c <= '9'); } static const std::string* FindKey(const KeyValuePairVector& kvp, const char* search) { for (const auto& it : kvp) { if (StringUtil::Strcasecmp(it.first.c_str(), search) == 0) return &it.second; } return nullptr; } bool CheatList::LoadFromPCSXRFile(const char* filename) { auto fp = FileSystem::OpenManagedCFile(filename, "rb"); if (!fp) return false; char line[1024]; CheatCode current_code; current_code.group = "Ungrouped"; while (std::fgets(line, sizeof(line), fp.get())) { char* start = line; while (*start != '\0' && std::isspace(*start)) start++; // skip empty lines if (*start == '\0') continue; char* end = start + std::strlen(start) - 1; while (end > start && std::isspace(*end)) { *end = '\0'; end--; } // skip comments and empty line if (*start == '#' || *start == ';' || *start == '/' || *start == '\"') continue; if (*start == '[' && *end == ']') { start++; *end = '\0'; // new cheat if (current_code.Valid()) m_codes.push_back(std::move(current_code)); current_code = CheatCode(); current_code.group = "Ungrouped"; if (*start == '*') { current_code.enabled = true; start++; } current_code.description.append(start); continue; } while (!IsHexCharacter(*start) && start != end) start++; if (start == end) continue; char* end_ptr; CheatCode::Instruction inst; inst.first = static_cast(std::strtoul(start, &end_ptr, 16)); inst.second = 0; if (end_ptr) { while (!IsHexCharacter(*end_ptr) && end_ptr != end) end_ptr++; if (end_ptr != end) inst.second = static_cast(std::strtoul(end_ptr, nullptr, 16)); } current_code.instructions.push_back(inst); } if (current_code.Valid()) m_codes.push_back(std::move(current_code)); Log_InfoPrintf("Loaded %zu cheats from '%s' (PCSXR format)", m_codes.size(), filename); return !m_codes.empty(); } bool CheatList::LoadFromLibretroFile(const char* filename) { auto fp = FileSystem::OpenManagedCFile(filename, "rb"); if (!fp) return false; char line[1024]; KeyValuePairVector kvp; while (std::fgets(line, sizeof(line), fp.get())) { char* start = line; while (*start != '\0' && std::isspace(*start)) start++; // skip empty lines if (*start == '\0' || *start == '=') continue; char* end = start + std::strlen(start) - 1; while (end > start && std::isspace(*end)) { *end = '\0'; end--; } char* equals = start; while (*equals != '=' && equals != end) equals++; if (equals == end) continue; *equals = '\0'; char* key_end = equals - 1; while (key_end > start && std::isspace(*key_end)) { *key_end = '\0'; key_end--; } char* value_start = equals + 1; while (*value_start != '\0' && std::isspace(*value_start)) value_start++; if (*value_start == '\0') continue; char* value_end = value_start + std::strlen(value_start) - 1; while (value_end > value_start && std::isspace(*value_end)) { *value_end = '\0'; value_end--; } if (*value_start == '\"') { if (*value_end != '\"') continue; value_start++; *value_end = '\0'; } kvp.emplace_back(start, value_start); } if (kvp.empty()) return false; const std::string* num_cheats_value = FindKey(kvp, "cheats"); const u32 num_cheats = num_cheats_value ? StringUtil::FromChars(*num_cheats_value).value_or(0) : 0; if (num_cheats == 0) return false; for (u32 i = 0; i < num_cheats; i++) { const std::string* desc = FindKey(kvp, TinyString::FromFormat("cheat%u_desc", i)); const std::string* code = FindKey(kvp, TinyString::FromFormat("cheat%u_code", i)); const std::string* enable = FindKey(kvp, TinyString::FromFormat("cheat%u_enable", i)); if (!desc || !code || !enable) { Log_WarningPrintf("Missing desc/code/enable for cheat %u in '%s'", i, filename); continue; } CheatCode cc; cc.group = "Ungrouped"; cc.description = *desc; cc.enabled = StringUtil::FromChars(*enable).value_or(false); if (ParseLibretroCheat(&cc, code->c_str())) m_codes.push_back(std::move(cc)); } Log_InfoPrintf("Loaded %zu cheats from '%s' (libretro format)", m_codes.size(), filename); return !m_codes.empty(); } static bool IsLibretroSeparator(char ch) { return (ch == ' ' || ch == '-' || ch == ':' || ch == '+'); } bool CheatList::ParseLibretroCheat(CheatCode* cc, const char* line) { const char* current_ptr = line; while (current_ptr) { char* end_ptr; CheatCode::Instruction inst; inst.first = static_cast(std::strtoul(current_ptr, &end_ptr, 16)); current_ptr = end_ptr; if (end_ptr) { if (!IsLibretroSeparator(*end_ptr)) { Log_WarningPrintf("Malformed code '%s'", line); break; } end_ptr++; inst.second = static_cast(std::strtoul(current_ptr, &end_ptr, 16)); if (end_ptr && *end_ptr == '\0') end_ptr = nullptr; if (end_ptr && *end_ptr != '\0') { if (!IsLibretroSeparator(*end_ptr)) { Log_WarningPrintf("Malformed code '%s'", line); break; } end_ptr++; } current_ptr = end_ptr; cc->instructions.push_back(inst); } } return !cc->instructions.empty(); } void CheatList::Apply() { for (const CheatCode& code : m_codes) { if (code.enabled) code.Apply(); } } void CheatList::AddCode(CheatCode cc) { m_codes.push_back(std::move(cc)); } void CheatList::SetCode(u32 index, CheatCode cc) { if (index > m_codes.size()) return; if (index == m_codes.size()) { m_codes.push_back(std::move(cc)); return; } m_codes[index] = std::move(cc); } void CheatList::RemoveCode(u32 i) { m_codes.erase(m_codes.begin() + i); } std::optional CheatList::DetectFileFormat(const char* filename) { auto fp = FileSystem::OpenManagedCFile(filename, "rb"); if (!fp) return Format::Count; char line[1024]; while (std::fgets(line, sizeof(line), fp.get())) { char* start = line; while (*start != '\0' && std::isspace(*start)) start++; // skip empty lines if (*start == '\0' || *start == '=') continue; char* end = start + std::strlen(start) - 1; while (end > start && std::isspace(*end)) { *end = '\0'; end--; } if (std::strncmp(line, "cheats", 6) == 0) return Format::Libretro; else return Format::PCSXR; } return Format::Count; } bool CheatList::LoadFromFile(const char* filename, Format format) { if (format == Format::Autodetect) format = DetectFileFormat(filename).value_or(Format::Count); if (format == Format::PCSXR) return LoadFromPCSXRFile(filename); else if (format == Format::Libretro) return LoadFromLibretroFile(filename); Log_ErrorPrintf("Invalid or unknown format for '%s'", filename); return false; } bool CheatList::SaveToPCSXRFile(const char* filename) { auto fp = FileSystem::OpenManagedCFile(filename, "wb"); if (!fp) return false; for (const CheatCode& cc : m_codes) { std::fprintf(fp.get(), "[%s%s]\n", cc.enabled ? "*" : "", cc.description.c_str()); for (const CheatCode::Instruction& i : cc.instructions) std::fprintf(fp.get(), "%08X %04X\n", i.first, i.second); std::fprintf(fp.get(), "\n"); } std::fflush(fp.get()); return (std::ferror(fp.get()) == 0); } u32 CheatList::GetEnabledCodeCount() const { u32 count = 0; for (const CheatCode& cc : m_codes) { if (cc.enabled) count++; } return count; } std::vector CheatList::GetCodeGroups() const { std::vector groups; for (const CheatCode& cc : m_codes) { if (std::any_of(groups.begin(), groups.end(), [cc](const std::string& group) { return (group == cc.group); })) continue; groups.emplace_back(cc.group); } return groups; } void CheatList::SetCodeEnabled(u32 index, bool state) { if (index >= m_codes.size()) return; m_codes[index].enabled = state; } void CheatList::EnableCode(u32 index) { SetCodeEnabled(index, true); } void CheatList::DisableCode(u32 index) { SetCodeEnabled(index, false); } void CheatList::ApplyCode(u32 index) { if (index >= m_codes.size()) return; m_codes[index].Apply(); } std::string CheatCode::GetInstructionsAsString() const { std::stringstream ss; for (const Instruction& inst : instructions) { ss << std::hex << std::uppercase << std::setw(8) << std::setfill('0') << inst.first; ss << " "; ss << std::hex << std::uppercase << std::setw(8) << std::setfill('0') << inst.second; ss << '\n'; } return ss.str(); } bool CheatCode::SetInstructionsFromString(const std::string& str) { std::vector new_instructions; std::istringstream ss(str); for (std::string line; std::getline(ss, line);) { char* start = line.data(); while (*start != '\0' && std::isspace(*start)) start++; // skip empty lines if (*start == '\0') continue; char* end = start + std::strlen(start) - 1; while (end > start && std::isspace(*end)) { *end = '\0'; end--; } // skip comments and empty line if (*start == '#' || *start == ';' || *start == '/' || *start == '\"') continue; while (!IsHexCharacter(*start) && start != end) start++; if (start == end) continue; char* end_ptr; CheatCode::Instruction inst; inst.first = static_cast(std::strtoul(start, &end_ptr, 16)); inst.second = 0; if (end_ptr) { while (!IsHexCharacter(*end_ptr) && end_ptr != end) end_ptr++; if (end_ptr != end) inst.second = static_cast(std::strtoul(end_ptr, nullptr, 16)); } new_instructions.push_back(inst); } if (new_instructions.empty()) return false; instructions = std::move(new_instructions); return true; } void CheatCode::Apply() const { const u32 count = static_cast(instructions.size()); u32 index = 0; for (; index < count;) { const Instruction& inst = instructions[index]; switch (inst.code) { case InstructionCode::Nop: { index++; } break; case InstructionCode::ConstantWrite8: { CPU::SafeWriteMemoryByte(inst.address, inst.value8); index++; } break; case InstructionCode::ConstantWrite16: { CPU::SafeWriteMemoryHalfWord(inst.address, inst.value16); index++; } break; case InstructionCode::ScratchpadWrite16: { CPU::SafeWriteMemoryHalfWord(CPU::DCACHE_LOCATION | (inst.address & CPU::DCACHE_OFFSET_MASK), inst.value16); index++; } break; case InstructionCode::Increment16: { u16 value = 0; CPU::SafeReadMemoryHalfWord(inst.address, &value); CPU::SafeWriteMemoryHalfWord(inst.address, value + inst.value16); index++; } break; case InstructionCode::Decrement16: { u16 value = 0; CPU::SafeReadMemoryHalfWord(inst.address, &value); CPU::SafeWriteMemoryHalfWord(inst.address, value - inst.value16); index++; } break; case InstructionCode::Increment8: { u8 value = 0; CPU::SafeReadMemoryByte(inst.address, &value); CPU::SafeWriteMemoryByte(inst.address, value + inst.value8); index++; } break; case InstructionCode::Decrement8: { u8 value = 0; CPU::SafeReadMemoryByte(inst.address, &value); CPU::SafeWriteMemoryByte(inst.address, value - inst.value8); index++; } break; case InstructionCode::CompareEqual16: { u16 value = 0; CPU::SafeReadMemoryHalfWord(inst.address, &value); if (value == inst.value16) index++; else index += 2; } break; case InstructionCode::CompareNotEqual16: { u16 value = 0; CPU::SafeReadMemoryHalfWord(inst.address, &value); if (value != inst.value16) index++; else index += 2; } break; case InstructionCode::CompareLess16: { u16 value = 0; CPU::SafeReadMemoryHalfWord(inst.address, &value); if (value < inst.value16) index++; else index += 2; } break; case InstructionCode::CompareGreater16: { u16 value = 0; CPU::SafeReadMemoryHalfWord(inst.address, &value); if (value > inst.value16) index++; else index += 2; } break; case InstructionCode::CompareEqual8: { u8 value = 0; CPU::SafeReadMemoryByte(inst.address, &value); if (value == inst.value8) index++; else index += 2; } break; case InstructionCode::CompareNotEqual8: { u8 value = 0; CPU::SafeReadMemoryByte(inst.address, &value); if (value != inst.value8) index++; else index += 2; } break; case InstructionCode::CompareLess8: { u8 value = 0; CPU::SafeReadMemoryByte(inst.address, &value); if (value < inst.value8) index++; else index += 2; } break; case InstructionCode::CompareGreater8: { u8 value = 0; CPU::SafeReadMemoryByte(inst.address, &value); if (value > inst.value8) index++; else index += 2; } break; case InstructionCode::Slide: { if ((index + 1) >= instructions.size()) { Log_ErrorPrintf("Incomplete slide instruction"); return; } const u32 slide_count = (inst.first >> 8) & 0xFFu; const u32 address_increment = SignExtendN<8>(inst.first & 0xFFu); const u16 value_increment = SignExtendN<8>(Truncate16(inst.second & 0xFFu)); const Instruction& inst2 = instructions[index + 1]; const InstructionCode write_type = inst2.code; u32 address = inst2.address; u16 value = inst2.value16; if (write_type == InstructionCode::ConstantWrite8) { for (u32 i = 0; i < slide_count; i++) { CPU::SafeWriteMemoryByte(address, Truncate8(value)); address += address_increment; value += value_increment; } } else if (write_type == InstructionCode::ConstantWrite16) { for (u32 i = 0; i < slide_count; i++) { CPU::SafeWriteMemoryHalfWord(address, value); address += address_increment; value += value_increment; } } else { Log_ErrorPrintf("Invalid command in second slide parameter 0x%02X", write_type); } index += 2; } break; case InstructionCode::MemoryCopy: { if ((index + 1) >= instructions.size()) { Log_ErrorPrintf("Incomplete memory copy instruction"); return; } const Instruction& inst2 = instructions[index + 1]; const u32 byte_count = inst.value16; u32 src_address = inst.address; u32 dst_address = inst2.address; for (u32 i = 0; i < byte_count; i++) { u8 value = 0; CPU::SafeReadMemoryByte(src_address, &value); CPU::SafeWriteMemoryByte(dst_address, value); src_address++; dst_address++; } index += 2; } break; default: { Log_ErrorPrintf("Unhandled instruction code 0x%02X (%08X %08X)", static_cast(inst.code.GetValue()), inst.first, inst.second); index++; } break; } } } static std::array s_cheat_code_type_names = {{"Gameshark"}}; static std::array s_cheat_code_type_display_names{{TRANSLATABLE("Cheats", "Gameshark")}}; const char* CheatCode::GetTypeName(Type type) { return s_cheat_code_type_names[static_cast(type)]; } const char* CheatCode::GetTypeDisplayName(Type type) { return s_cheat_code_type_display_names[static_cast(type)]; } std::optional CheatCode::ParseTypeName(const char* str) { for (u32 i = 0; i < static_cast(s_cheat_code_type_names.size()); i++) { if (std::strcmp(s_cheat_code_type_names[i], str) == 0) return static_cast(i); } return std::nullopt; } static std::array s_cheat_code_activation_names = {{"Manual", "EndFrame"}}; static std::array s_cheat_code_activation_display_names{ {TRANSLATABLE("Cheats", "Manual"), TRANSLATABLE("Cheats", "Automatic (Frame End)")}}; const char* CheatCode::GetActivationName(Activation activation) { return s_cheat_code_activation_names[static_cast(activation)]; } const char* CheatCode::GetActivationDisplayName(Activation activation) { return s_cheat_code_activation_display_names[static_cast(activation)]; } std::optional CheatCode::ParseActivationName(const char* str) { for (u32 i = 0; i < static_cast(s_cheat_code_activation_names.size()); i++) { if (std::strcmp(s_cheat_code_activation_names[i], str) == 0) return static_cast(i); } return std::nullopt; } MemoryScan::MemoryScan() = default; MemoryScan::~MemoryScan() = default; void MemoryScan::ResetSearch() { m_results.clear(); } void MemoryScan::Search() { m_results.clear(); switch (m_size) { case MemoryAccessSize::Byte: SearchBytes(); break; case MemoryAccessSize::HalfWord: SearchHalfwords(); break; case MemoryAccessSize::Word: SearchWords(); break; default: break; } } void MemoryScan::SearchBytes() { for (PhysicalMemoryAddress address = m_start_address; address < m_end_address; address++) { u8 bvalue = 0; CPU::SafeReadMemoryByte(address, &bvalue); Result res; res.address = address; res.value = m_signed ? SignExtend32(bvalue) : ZeroExtend32(bvalue); res.last_value = res.value; res.value_changed = false; if (res.Filter(m_operator, m_value, m_signed)) m_results.push_back(res); } } void MemoryScan::SearchHalfwords() { for (PhysicalMemoryAddress address = m_start_address; address < m_end_address; address += 2) { u16 bvalue = 0; CPU::SafeReadMemoryHalfWord(address, &bvalue); Result res; res.address = address; res.value = m_signed ? SignExtend32(bvalue) : ZeroExtend32(bvalue); res.last_value = res.value; res.value_changed = false; if (res.Filter(m_operator, m_value, m_signed)) m_results.push_back(res); } } void MemoryScan::SearchWords() { for (PhysicalMemoryAddress address = m_start_address; address < m_end_address; address += 4) { Result res; res.address = address; CPU::SafeReadMemoryWord(address, &res.value); res.last_value = res.value; res.value_changed = false; if (res.Filter(m_operator, m_value, m_signed)) m_results.push_back(res); } } void MemoryScan::SearchAgain() { ResultVector new_results; new_results.reserve(m_results.size()); for (Result& res : m_results) { res.UpdateValue(m_size, m_signed); if (res.Filter(m_operator, m_value, m_signed)) { res.last_value = res.value; new_results.push_back(res); } } m_results.swap(new_results); } void MemoryScan::UpdateResultsValues() { for (Result& res : m_results) res.UpdateValue(m_size, m_signed); } void MemoryScan::SetResultValue(u32 index, u32 value) { if (index >= m_results.size()) return; Result& res = m_results[index]; if (res.value == value) return; switch (m_size) { case MemoryAccessSize::Byte: CPU::SafeWriteMemoryByte(res.address, Truncate8(value)); break; case MemoryAccessSize::HalfWord: CPU::SafeWriteMemoryHalfWord(res.address, Truncate16(value)); break; case MemoryAccessSize::Word: CPU::SafeWriteMemoryWord(res.address, value); break; } res.value = value; res.value_changed = true; } bool MemoryScan::Result::Filter(Operator op, u32 comp_value, bool is_signed) const { switch (op) { case Operator::Equal: { return (value == comp_value); } case Operator::NotEqual: { return (value != comp_value); } case Operator::GreaterThan: { return is_signed ? (static_cast(value) > static_cast(comp_value)) : (value > comp_value); } case Operator::GreaterEqual: { return is_signed ? (static_cast(value) >= static_cast(comp_value)) : (value >= comp_value); } case Operator::LessThan: { return is_signed ? (static_cast(value) < static_cast(comp_value)) : (value < comp_value); } case Operator::LessEqual: { return is_signed ? (static_cast(value) <= static_cast(comp_value)) : (value <= comp_value); } case Operator::IncreasedBy: { return is_signed ? ((static_cast(value) - static_cast(last_value)) == static_cast(comp_value)) : ((value - last_value) == comp_value); } case Operator::DecreasedBy: { return is_signed ? ((static_cast(last_value) - static_cast(value)) == static_cast(comp_value)) : ((last_value - value) == comp_value); } case Operator::ChangedBy: { if (is_signed) return (std::abs(static_cast(last_value) - static_cast(value)) == static_cast(comp_value)); else return ((last_value > value) ? (last_value - value) : (value - last_value)) == comp_value; } case Operator::EqualLast: { return (value == last_value); } case Operator::NotEqualLast: { return (value != last_value); } case Operator::GreaterThanLast: { return is_signed ? (static_cast(value) > static_cast(last_value)) : (value > last_value); } case Operator::GreaterEqualLast: { return is_signed ? (static_cast(value) >= static_cast(last_value)) : (value >= last_value); } case Operator::LessThanLast: { return is_signed ? (static_cast(value) < static_cast(last_value)) : (value < last_value); } case Operator::LessEqualLast: { return is_signed ? (static_cast(value) <= static_cast(last_value)) : (value <= last_value); } default: return false; } } void MemoryScan::Result::UpdateValue(MemoryAccessSize size, bool is_signed) { const u32 old_value = value; switch (size) { case MemoryAccessSize::Byte: { u8 bvalue = 0; CPU::SafeReadMemoryByte(address, &bvalue); value = is_signed ? SignExtend32(bvalue) : ZeroExtend32(bvalue); } break; case MemoryAccessSize::HalfWord: { u16 bvalue = 0; CPU::SafeReadMemoryHalfWord(address, &bvalue); value = is_signed ? SignExtend32(bvalue) : ZeroExtend32(bvalue); } break; case MemoryAccessSize::Word: { CPU::SafeReadMemoryWord(address, &value); } break; } value_changed = (value != old_value); } MemoryWatchList::MemoryWatchList() = default; MemoryWatchList::~MemoryWatchList() = default; const MemoryWatchList::Entry* MemoryWatchList::GetEntryByAddress(u32 address) const { for (const Entry& entry : m_entries) { if (entry.address == address) return &entry; } return nullptr; } bool MemoryWatchList::AddEntry(std::string description, u32 address, MemoryAccessSize size, bool is_signed, bool freeze) { if (GetEntryByAddress(address)) return false; Entry entry; entry.description = std::move(description); entry.address = address; entry.size = size; entry.is_signed = is_signed; entry.freeze = false; UpdateEntryValue(&entry); entry.changed = false; entry.freeze = freeze; m_entries.push_back(std::move(entry)); return true; } void MemoryWatchList::RemoveEntry(u32 index) { if (index >= m_entries.size()) return; m_entries.erase(m_entries.begin() + index); } bool MemoryWatchList::RemoveEntryByAddress(u32 address) { for (auto it = m_entries.begin(); it != m_entries.end(); ++it) { if (it->address == address) { m_entries.erase(it); return true; } } return false; } void MemoryWatchList::SetEntryDescription(u32 index, std::string description) { if (index >= m_entries.size()) return; Entry& entry = m_entries[index]; entry.description = std::move(description); } void MemoryWatchList::SetEntryFreeze(u32 index, bool freeze) { if (index >= m_entries.size()) return; Entry& entry = m_entries[index]; entry.freeze = freeze; } void MemoryWatchList::SetEntryValue(u32 index, u32 value) { if (index >= m_entries.size()) return; Entry& entry = m_entries[index]; if (entry.value == value) return; SetEntryValue(&entry, value); } bool MemoryWatchList::RemoveEntryByDescription(const char* description) { bool result = false; for (auto it = m_entries.begin(); it != m_entries.end();) { if (it->description == description) { it = m_entries.erase(it); result = true; continue; } ++it; } return result; } void MemoryWatchList::UpdateValues() { for (Entry& entry : m_entries) UpdateEntryValue(&entry); } void MemoryWatchList::SetEntryValue(Entry* entry, u32 value) { switch (entry->size) { case MemoryAccessSize::Byte: CPU::SafeWriteMemoryByte(entry->address, Truncate8(value)); break; case MemoryAccessSize::HalfWord: CPU::SafeWriteMemoryHalfWord(entry->address, Truncate16(value)); break; case MemoryAccessSize::Word: CPU::SafeWriteMemoryWord(entry->address, value); break; } entry->changed = (entry->value != value); entry->value = value; } void MemoryWatchList::UpdateEntryValue(Entry* entry) { const u32 old_value = entry->value; switch (entry->size) { case MemoryAccessSize::Byte: { u8 bvalue = 0; CPU::SafeReadMemoryByte(entry->address, &bvalue); entry->value = entry->is_signed ? SignExtend32(bvalue) : ZeroExtend32(bvalue); } break; case MemoryAccessSize::HalfWord: { u16 bvalue = 0; CPU::SafeReadMemoryHalfWord(entry->address, &bvalue); entry->value = entry->is_signed ? SignExtend32(bvalue) : ZeroExtend32(bvalue); } break; case MemoryAccessSize::Word: { CPU::SafeReadMemoryWord(entry->address, &entry->value); } break; } entry->changed = (old_value != entry->value); if (entry->freeze && entry->changed) SetEntryValue(entry, old_value); }