diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index c8e2f074a..5994347cd 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -9,6 +9,8 @@ add_library(core cdrom.h cdrom_async_reader.cpp cdrom_async_reader.h + cheats.cpp + cheats.h controller.cpp controller.h cpu_code_cache.cpp diff --git a/src/core/cheats.cpp b/src/core/cheats.cpp new file mode 100644 index 000000000..873e34b4b --- /dev/null +++ b/src/core/cheats.cpp @@ -0,0 +1,483 @@ +#include "cheats.h" +#include "common/file_system.h" +#include "common/log.h" +#include "common/string.h" +#include "common/string_util.h" +#include "cpu_core.h" +#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; + 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 = {}; + current_code.enabled = false; + 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 == end) + 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 == value_end) + continue; + + 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 = StringUtil::FromChars(*num_cheats_value).value_or(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.description = *desc; + cc.enabled = StringUtil::FromChars(*enable).value_or(false); + + const char* current_ptr = code->c_str(); + 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 (*end_ptr != ' ') + { + Log_WarningPrintf("Malformed code '%s'", code->c_str()); + break; + } + + end_ptr++; + inst.second = static_cast(std::strtoul(current_ptr, &end_ptr, 16)); + current_ptr = end_ptr; + + if (end_ptr) + { + if (*end_ptr != '+') + { + Log_WarningPrintf("Malformed code '%s'", code->c_str()); + break; + } + + end_ptr++; + current_ptr = end_ptr; + } + + cc.instructions.push_back(inst); + } + } + + m_codes.push_back(std::move(cc)); + } + + Log_InfoPrintf("Loaded %zu cheats from '%s' (libretro format)", m_codes.size(), filename); + return !m_codes.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::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]; + 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--; + } + + 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); +} + +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::ConstantWrite8: + { + CPU::SafeWriteMemoryByte(inst.address, inst.value8); + index++; + } + break; + + case InstructionCode::ConstantWrite16: + { + CPU::SafeWriteMemoryHalfWord(inst.address, inst.value16); + index++; + } + break; + + case InstructionCode::Increment16: + { + u16 value = 0; + CPU::SafeReadMemoryHalfWord(inst.address, &value); + CPU::SafeWriteMemoryHalfWord(inst.address, value + 1u); + index++; + } + break; + + case InstructionCode::Decrement16: + { + u16 value = 0; + CPU::SafeReadMemoryHalfWord(inst.address, &value); + CPU::SafeWriteMemoryHalfWord(inst.address, value - 1u); + index++; + } + break; + + case InstructionCode::Increment8: + { + u8 value = 0; + CPU::SafeReadMemoryByte(inst.address, &value); + CPU::SafeWriteMemoryByte(inst.address, value + 1u); + index++; + } + break; + + case InstructionCode::Decrement8: + { + u8 value = 0; + CPU::SafeReadMemoryByte(inst.address, &value); + CPU::SafeWriteMemoryByte(inst.address, value - 1u); + 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; + + default: + { + Log_ErrorPrintf("Unhandled instruction code 0x%02X (%08X %08X)", static_cast(inst.code.GetValue()), + inst.first, inst.second); + index++; + } + break; + } + } +} diff --git a/src/core/cheats.h b/src/core/cheats.h new file mode 100644 index 000000000..058f3a7ab --- /dev/null +++ b/src/core/cheats.h @@ -0,0 +1,87 @@ +#pragma once +#include "common/bitfield.h" +#include "types.h" +#include +#include +#include + +struct CheatCode +{ + enum class InstructionCode : u8 + { + ConstantWrite8 = 0x30, + ConstantWrite16 = 0x80, + Increment16 = 0x10, + Decrement16 = 0x11, + Increment8 = 0x20, + Decrement8 = 0x21, + CompareEqual16 = 0xD0, + CompareNotEqual16 = 0xD1, + CompareLess16 = 0xD2, + CompareGreater16 = 0xD3, + CompareEqual8 = 0xE0, + CompareNotEqual8 = 0xE1, + CompareLess8 = 0xE2, + CompareGreater8 = 0xE3, + Slide = 0x50 + }; + + union Instruction + { + u64 bits; + + struct + { + u32 second; + u32 first; + }; + + BitField code; + BitField address; + BitField value16; + BitField value8; + }; + + std::string description; + std::vector instructions; + bool enabled; + + ALWAYS_INLINE bool Valid() const { return !instructions.empty() && !description.empty(); } + + void Apply() const; +}; + +class CheatList final +{ +public: + enum class Format + { + Autodetect, + PCSXR, + Libretro, + Count + }; + + CheatList(); + ~CheatList(); + + ALWAYS_INLINE const CheatCode& GetCode(u32 i) const { return m_codes[i]; } + ALWAYS_INLINE CheatCode& GetCode(u32 i) { return m_codes[i]; } + ALWAYS_INLINE u32 GetCodeCount() const { return static_cast(m_codes.size()); } + + void AddCode(CheatCode cc); + void RemoveCode(u32 i); + + static std::optional DetectFileFormat(const char* filename); + + bool LoadFromFile(const char* filename, Format format); + bool LoadFromPCSXRFile(const char* filename); + bool LoadFromLibretroFile(const char* filename); + + bool SaveToPCSXRFile(const char* filename); + + void Apply(); + +private: + std::vector m_codes; +}; diff --git a/src/core/core.vcxproj b/src/core/core.vcxproj index cd02f72f2..e1fe31cd2 100644 --- a/src/core/core.vcxproj +++ b/src/core/core.vcxproj @@ -40,6 +40,7 @@ + @@ -96,6 +97,7 @@ + diff --git a/src/core/core.vcxproj.filters b/src/core/core.vcxproj.filters index 96b2c050b..e928a6f1c 100644 --- a/src/core/core.vcxproj.filters +++ b/src/core/core.vcxproj.filters @@ -46,6 +46,7 @@ + @@ -95,5 +96,6 @@ + \ No newline at end of file diff --git a/src/core/system.cpp b/src/core/system.cpp index 25c6ad017..a86dd6f1b 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -2,6 +2,7 @@ #include "bios.h" #include "bus.h" #include "cdrom.h" +#include "cheats.h" #include "common/audio_stream.h" #include "common/file_system.h" #include "common/iso_reader.h" @@ -26,8 +27,8 @@ #include "sio.h" #include "spu.h" #include "timers.h" -#include #include +#include #include #include Log_SetChannel(System); @@ -103,6 +104,8 @@ static Common::Timer s_frame_timer; static std::vector s_media_playlist; static std::string s_media_playlist_filename; +static std::unique_ptr s_cheat_list; + State GetState() { return s_state; @@ -1107,6 +1110,9 @@ void RunFrame() // Generate any pending samples from the SPU before sleeping, this way we reduce the chances of underruns. g_spu.GeneratePendingSamples(); + if (s_cheat_list) + s_cheat_list->Apply(); + g_gpu->ResetGraphicsAPIState(); } @@ -1643,4 +1649,26 @@ bool SwitchMediaFromPlaylist(u32 index) return InsertMedia(path.c_str()); } +bool HasCheatList() +{ + return static_cast(s_cheat_list); +} + +CheatList* GetCheatList() +{ + return s_cheat_list.get(); +} + +void ApplyCheatCode(const CheatCode& code) +{ + Assert(!IsShutdown()); + code.Apply(); +} + +void SetCheatList(std::unique_ptr cheats) +{ + Assert(!IsShutdown()); + s_cheat_list = std::move(cheats); +} + } // namespace System \ No newline at end of file diff --git a/src/core/system.h b/src/core/system.h index a6fde2ac5..938bee0d7 100644 --- a/src/core/system.h +++ b/src/core/system.h @@ -13,6 +13,9 @@ class StateWrapper; class Controller; +struct CheatCode; +class CheatList; + struct SystemBootParameters { SystemBootParameters(); @@ -156,4 +159,16 @@ bool ReplaceMediaPathFromPlaylist(u32 index, const std::string_view& path); /// Switches to the specified media/disc playlist index. bool SwitchMediaFromPlaylist(u32 index); +/// Returns true if there is currently a cheat list. +bool HasCheatList(); + +/// Accesses the current cheat list. +CheatList* GetCheatList(); + +/// Applies a single cheat code. +void ApplyCheatCode(const CheatCode& code); + +/// Sets or clears the provided cheat list, applying every frame. +void SetCheatList(std::unique_ptr cheats); + } // namespace System