Add pcsxr and libretro cheat list parsing

This commit is contained in:
Connor McLaughlin 2020-09-09 22:11:28 +10:00
parent c2e7e8254f
commit ddb38ac31d
7 changed files with 620 additions and 1 deletions

View file

@ -9,6 +9,8 @@ add_library(core
cdrom.h cdrom.h
cdrom_async_reader.cpp cdrom_async_reader.cpp
cdrom_async_reader.h cdrom_async_reader.h
cheats.cpp
cheats.h
controller.cpp controller.cpp
controller.h controller.h
cpu_code_cache.cpp cpu_code_cache.cpp

483
src/core/cheats.cpp Normal file
View file

@ -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 <cctype>
Log_SetChannel(Cheats);
using KeyValuePairVector = std::vector<std::pair<std::string, std::string>>;
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<u32>(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<u32>(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<u32>(*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<bool>(*enable).value_or(false);
const char* current_ptr = code->c_str();
while (current_ptr)
{
char* end_ptr;
CheatCode::Instruction inst;
inst.first = static_cast<u32>(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<u32>(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::Format> 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<u32>(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<u8>(inst.code.GetValue()),
inst.first, inst.second);
index++;
}
break;
}
}
}

87
src/core/cheats.h Normal file
View file

@ -0,0 +1,87 @@
#pragma once
#include "common/bitfield.h"
#include "types.h"
#include <optional>
#include <string>
#include <vector>
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<u64, InstructionCode, 32 + 24, 8> code;
BitField<u64, u32, 32, 24> address;
BitField<u64, u16, 0, 16> value16;
BitField<u64, u8, 0, 8> value8;
};
std::string description;
std::vector<Instruction> 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<u32>(m_codes.size()); }
void AddCode(CheatCode cc);
void RemoveCode(u32 i);
static std::optional<Format> 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<CheatCode> m_codes;
};

View file

@ -40,6 +40,7 @@
<ClCompile Include="bus.cpp" /> <ClCompile Include="bus.cpp" />
<ClCompile Include="cdrom.cpp" /> <ClCompile Include="cdrom.cpp" />
<ClCompile Include="cdrom_async_reader.cpp" /> <ClCompile Include="cdrom_async_reader.cpp" />
<ClCompile Include="cheats.cpp" />
<ClCompile Include="cpu_core.cpp" /> <ClCompile Include="cpu_core.cpp" />
<ClCompile Include="cpu_disasm.cpp" /> <ClCompile Include="cpu_disasm.cpp" />
<ClCompile Include="cpu_code_cache.cpp" /> <ClCompile Include="cpu_code_cache.cpp" />
@ -96,6 +97,7 @@
<ClInclude Include="bus.h" /> <ClInclude Include="bus.h" />
<ClInclude Include="cdrom.h" /> <ClInclude Include="cdrom.h" />
<ClInclude Include="cdrom_async_reader.h" /> <ClInclude Include="cdrom_async_reader.h" />
<ClInclude Include="cheats.h" />
<ClInclude Include="cpu_core.h" /> <ClInclude Include="cpu_core.h" />
<ClInclude Include="cpu_core_private.h" /> <ClInclude Include="cpu_core_private.h" />
<ClInclude Include="cpu_disasm.h" /> <ClInclude Include="cpu_disasm.h" />

View file

@ -46,6 +46,7 @@
<ClCompile Include="resources.cpp" /> <ClCompile Include="resources.cpp" />
<ClCompile Include="host_interface_progress_callback.cpp" /> <ClCompile Include="host_interface_progress_callback.cpp" />
<ClCompile Include="pgxp.cpp" /> <ClCompile Include="pgxp.cpp" />
<ClCompile Include="cheats.cpp" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="types.h" /> <ClInclude Include="types.h" />
@ -95,5 +96,6 @@
<ClInclude Include="gte_types.h" /> <ClInclude Include="gte_types.h" />
<ClInclude Include="pgxp.h" /> <ClInclude Include="pgxp.h" />
<ClInclude Include="cpu_core_private.h" /> <ClInclude Include="cpu_core_private.h" />
<ClInclude Include="cheats.h" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -2,6 +2,7 @@
#include "bios.h" #include "bios.h"
#include "bus.h" #include "bus.h"
#include "cdrom.h" #include "cdrom.h"
#include "cheats.h"
#include "common/audio_stream.h" #include "common/audio_stream.h"
#include "common/file_system.h" #include "common/file_system.h"
#include "common/iso_reader.h" #include "common/iso_reader.h"
@ -26,8 +27,8 @@
#include "sio.h" #include "sio.h"
#include "spu.h" #include "spu.h"
#include "timers.h" #include "timers.h"
#include <cstdio>
#include <cctype> #include <cctype>
#include <cstdio>
#include <fstream> #include <fstream>
#include <limits> #include <limits>
Log_SetChannel(System); Log_SetChannel(System);
@ -103,6 +104,8 @@ static Common::Timer s_frame_timer;
static std::vector<std::string> s_media_playlist; static std::vector<std::string> s_media_playlist;
static std::string s_media_playlist_filename; static std::string s_media_playlist_filename;
static std::unique_ptr<CheatList> s_cheat_list;
State GetState() State GetState()
{ {
return s_state; 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. // Generate any pending samples from the SPU before sleeping, this way we reduce the chances of underruns.
g_spu.GeneratePendingSamples(); g_spu.GeneratePendingSamples();
if (s_cheat_list)
s_cheat_list->Apply();
g_gpu->ResetGraphicsAPIState(); g_gpu->ResetGraphicsAPIState();
} }
@ -1643,4 +1649,26 @@ bool SwitchMediaFromPlaylist(u32 index)
return InsertMedia(path.c_str()); return InsertMedia(path.c_str());
} }
bool HasCheatList()
{
return static_cast<bool>(s_cheat_list);
}
CheatList* GetCheatList()
{
return s_cheat_list.get();
}
void ApplyCheatCode(const CheatCode& code)
{
Assert(!IsShutdown());
code.Apply();
}
void SetCheatList(std::unique_ptr<CheatList> cheats)
{
Assert(!IsShutdown());
s_cheat_list = std::move(cheats);
}
} // namespace System } // namespace System

View file

@ -13,6 +13,9 @@ class StateWrapper;
class Controller; class Controller;
struct CheatCode;
class CheatList;
struct SystemBootParameters struct SystemBootParameters
{ {
SystemBootParameters(); SystemBootParameters();
@ -156,4 +159,16 @@ bool ReplaceMediaPathFromPlaylist(u32 index, const std::string_view& path);
/// Switches to the specified media/disc playlist index. /// Switches to the specified media/disc playlist index.
bool SwitchMediaFromPlaylist(u32 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<CheatList> cheats);
} // namespace System } // namespace System