GameShark cheat codes support

This commit is contained in:
Flyinghead 2021-10-10 17:24:17 +02:00
parent 8b986dfab3
commit 163888f329
11 changed files with 808 additions and 178 deletions

View File

@ -1325,6 +1325,8 @@ if(BUILD_TESTING)
core/deps/gtest/src/gtest_main.cc)
target_sources(${PROJECT_NAME} PRIVATE
tests/src/CheatManagerTest.cpp
tests/src/ConfigFileTest.cpp
tests/src/div32_test.cpp
tests/src/test_stubs.cpp
tests/src/serialize_test.cpp

View File

@ -1,8 +1,5 @@
/*
Command line parsing
~yay~
Nothing too interesting here, really
*/
#include <cstdio>
@ -10,39 +7,9 @@
#include <cstring>
#include "cfg/cfg.h"
#include "stdclass.h"
char* trim_ws(char* str)
{
if (str==0 || strlen(str)==0)
return 0;
while(*str)
{
if (!isspace(*str))
break;
str++;
}
size_t l=strlen(str);
if (l==0)
return 0;
while(l>0)
{
if (!isspace(str[l-1]))
break;
str[l-1]=0;
l--;
}
if (l==0)
return 0;
return str;
}
int setconfig(char** arg,int cl)
static int setconfig(char *arg[], int cl)
{
int rv=0;
for(;;)
@ -50,43 +17,38 @@ int setconfig(char** arg,int cl)
if (cl<1)
{
WARN_LOG(COMMON, "-config : invalid number of parameters, format is section:key=value");
return rv;
break;
}
char* sep=strstr(arg[1],":");
if (sep==0)
std::string value(arg[1]);
auto seppos = value.find(':');
if (seppos == std::string::npos)
{
WARN_LOG(COMMON, "-config : invalid parameter %s, format is section:key=value", arg[1]);
return rv;
WARN_LOG(COMMON, "-config : invalid parameter %s, format is section:key=value", value.c_str());
break;
}
char* value=strstr(sep+1,"=");
if (value==0)
auto eqpos = value.find('=', seppos);
if (eqpos == std::string::npos)
{
WARN_LOG(COMMON, "-config : invalid parameter %s, format is section:key=value", arg[1]);
return rv;
WARN_LOG(COMMON, "-config : invalid parameter %s, format is section:key=value", value.c_str());
break;
}
*sep++=0;
*value++=0;
std::string sect = trim_ws(value.substr(0, seppos));
std::string key = trim_ws(value.substr(seppos + 1, eqpos - seppos - 1));
value = trim_ws(value.substr(eqpos + 1));
char* sect=trim_ws(arg[1]);
char* key=trim_ws(sep);
value=trim_ws(value);
if (sect==0 || key==0)
if (sect.empty() || key.empty())
{
WARN_LOG(COMMON, "-config : invalid parameter, format is section:key=value");
return rv;
break;
}
const char* constval = value;
if (constval==0)
constval="";
INFO_LOG(COMMON, "Virtual cfg %s:%s=%s", sect, key, constval);
INFO_LOG(COMMON, "Virtual cfg %s:%s=%s", sect.c_str(), key.c_str(), value.c_str());
cfgSetVirtual(sect, key, constval);
cfgSetVirtual(sect, key, value);
rv++;
if (cl>=3 && stricmp(arg[2],",")==0)
if (cl>=3 && strcmp(arg[2],",")==0)
{
cl-=2;
arg+=2;

View File

@ -1,8 +1,7 @@
#include "ini.h"
#include "types.h"
#include <sstream>
char* trim_ws(char* str);
#include "stdclass.h"
namespace emucfg {
@ -197,74 +196,37 @@ void ConfigFile::set_bool(const std::string& section_name, const std::string& en
void ConfigFile::parse(FILE* file)
{
if(file == NULL)
{
if (file == nullptr)
return;
}
char line[512];
char current_section[512] = { '\0' };
std::string section;
int cline = 0;
while(file && !feof(file))
while (true)
{
if (std::fgets(line, 512, file) == NULL || std::feof(file))
{
if (std::fgets(line, sizeof(line), file) == nullptr)
break;
}
cline++;
if (strlen(line) < 3)
std::string s(line);
s = trim_ws(s, " \r\n");
if (s.empty())
continue;
if (s.length() >= 3 && s[0] == '[' && s[s.length() - 1] == ']')
{
section = s.substr(1, s.length() - 2);
continue;
}
if (line[strlen(line)-1] == '\r' ||
line[strlen(line)-1] == '\n')
auto eqpos = s.find('=');
if (eqpos == std::string::npos)
{
line[strlen(line)-1] = '\0';
}
char* tl = trim_ws(line);
if (tl[0] == '[' && tl[strlen(tl)-1] == ']')
{
tl[strlen(tl)-1] = '\0';
// FIXME: Data loss if buffer is too small
strncpy(current_section, tl+1, sizeof(current_section));
current_section[sizeof(current_section) - 1] = '\0';
trim_ws(current_section);
}
else
{
if (strlen(current_section) == 0)
{
continue; //no open section
}
char* separator = strstr(tl, "=");
if (!separator)
{
WARN_LOG(COMMON, "Malformed entry on config - ignoring @ %d(%s)", cline, tl);
continue;
}
*separator = '\0';
char* name = trim_ws(tl);
char* value = trim_ws(separator + 1);
if (name == NULL || value == NULL)
{
//printf("Malformed entry on config - ignoring @ %d(%s)\n",cline, tl);
continue;
}
else
{
this->set(std::string(current_section), std::string(name), std::string(value));
}
WARN_LOG(COMMON, "Malformed entry on config - ignoring line %d: %s", cline, s.c_str());
continue;
}
std::string property = trim_ws(s.substr(0, eqpos));
std::string value = trim_ws(s.substr(eqpos + 1));
if (value.length() >= 2 && value[0] == '"' && value[value.length() - 1] == '"')
value = value.substr(1, value.length() - 2);
set(section, property, value);
}
}
@ -275,7 +237,8 @@ void ConfigFile::save(FILE* file)
const std::string& section_name = section_it.first;
const ConfigSection& section = section_it.second;
std::fprintf(file, "[%s]\n", section_name.c_str());
if (!section_name.empty())
std::fprintf(file, "[%s]\n", section_name.c_str());
for (const auto& entry_it : section.entries)
{
@ -283,8 +246,6 @@ void ConfigFile::save(FILE* file)
const ConfigEntry& entry = entry_it.second;
std::fprintf(file, "%s = %s\n", entry_name.c_str(), entry.get_string().c_str());
}
std::fputs("\n", file);
}
}

View File

@ -24,6 +24,7 @@
#include "hw/sh4/sh4_mem.h"
#include "reios/reios.h"
#include "cfg/cfg.h"
#include "cfg/ini.h"
const WidescreenCheat CheatManager::widescreen_cheats[] =
{
@ -312,60 +313,39 @@ void CheatManager::loadCheatFile(const std::string& filename)
WARN_LOG(COMMON, "Cannot open cheat file '%s'", filename.c_str());
return;
}
cheats.clear();
Cheat cheat;
int cheatNumber = 0;
char buffer[512];
while (fgets(buffer, sizeof(buffer), cheatfile) != nullptr)
{
std::string l = buffer;
auto equalPos = l.find('=');
if (equalPos == std::string::npos)
continue;
auto quotePos = l.find('"', equalPos);
if (quotePos == std::string::npos)
continue;
auto quote2Pos = l.find('"', quotePos + 1);
if (quote2Pos == std::string::npos)
continue;
if (l.substr(0, 5) != "cheat" || l[5] < '0' || l[5] > '9')
continue;
char *p;
int number = strtol(&l[5], &p, 10);
if (number != cheatNumber && cheat.type != Cheat::Type::disabled)
{
cheatNumber = number;
cheats.push_back(cheat);
cheat = Cheat();
}
std::string param = trim_trailing_ws(l.substr(p - &l[0], equalPos - (p - &l[0])));
std::string value = l.substr(quotePos + 1, quote2Pos - quotePos - 1);
emucfg::ConfigFile cfg;
cfg.parse(cheatfile);
fclose(cheatfile);
if (param == "_address")
int count = cfg.get_int("", "cheats", 0);
cheats.clear();
for (int i = 0; i < count; i++)
{
std::string prefix = "cheat" + std::to_string(i) + "_";
Cheat cheat{};
cheat.description = cfg.get("", prefix + "desc", "Cheat " + std::to_string(i + 1));
cheat.address = cfg.get_int("", prefix + "address", -1);
if (cheat.address >= RAM_SIZE)
{
cheat.address = strtol(value.c_str(), nullptr, 10);
verify(cheat.address < RAM_SIZE);
WARN_LOG(COMMON, "Invalid address %x", cheat.address);
continue;
}
else if (param == "_cheat_type")
cheat.type = (Cheat::Type)strtol(value.c_str(), nullptr, 10);
else if (param == "_desc")
cheat.description = value;
else if (param == "_memory_search_size")
cheat.size = 1 << strtol(value.c_str(), nullptr, 10);
else if (param == "_value")
cheat.value = strtol(value.c_str(), nullptr, 10);
else if (param == "_repeat_count")
cheat.repeatCount = strtol(value.c_str(), nullptr, 10);
else if (param == "_repeat_add_to_value")
cheat.repeatValueIncrement = strtol(value.c_str(), nullptr, 10);
else if (param == "_repeat_add_to_address")
cheat.repeatAddressIncrement = strtol(value.c_str(), nullptr, 10);
else if (param == "_enable")
cheat.enabled = value == "true";
cheat.type = (Cheat::Type)cfg.get_int("", prefix + "cheat_type", (int)Cheat::Type::disabled);
cheat.size = 1 << cfg.get_int("", prefix + "memory_search_size", 0);
cheat.value = cfg.get_int("", prefix + "value", cheat.value);
cheat.repeatCount = cfg.get_int("", prefix + "repeat_count", cheat.repeatCount);
cheat.repeatValueIncrement = cfg.get_int("", prefix + "repeat_add_to_value", cheat.repeatValueIncrement);
cheat.repeatAddressIncrement = cfg.get_int("", prefix + "repeat_add_to_address", cheat.repeatAddressIncrement);
cheat.enabled = cfg.get_bool("", prefix + "enable", false);
cheat.destAddress = cfg.get_int("", prefix + "dest_address", 0);
if (cheat.destAddress >= RAM_SIZE)
{
WARN_LOG(COMMON, "Invalid address %x", cheat.destAddress);
continue;
}
if (cheat.type != Cheat::Type::disabled)
cheats.push_back(cheat);
}
std::fclose(cheatfile);
if (cheat.type != Cheat::Type::disabled)
cheats.push_back(cheat);
active = !cheats.empty();
INFO_LOG(COMMON, "%d cheats loaded", (int)cheats.size());
cfgSaveStr("cheats", gameId, filename);
@ -506,6 +486,10 @@ void CheatManager::apply()
case Cheat::Type::runNextIfLt:
skipCheat = readRam(cheat.address, cheat.size) >= cheat.value;
break;
case Cheat::Type::copy:
for (u32 i = 0; i < cheat.repeatCount; i++)
writeRam(cheat.destAddress + i, readRam(cheat.address + i, cheat.size), cheat.size);
break;
}
if (setValue)
{
@ -520,3 +504,289 @@ void CheatManager::apply()
}
}
}
static std::vector<u32> parseCodes(const std::string& s)
{
std::vector<u32> codes;
std::string curCode;
for (u8 c : s)
{
if (std::isxdigit(c))
{
curCode += c;
if (curCode.length() == 8)
{
codes.push_back(strtol(curCode.c_str(), nullptr, 16));
curCode.clear();
}
}
else if (!curCode.empty())
throw FlycastException("Invalid cheat code");
}
if (!curCode.empty())
{
if (curCode.length() != 8)
throw FlycastException("Invalid cheat code");
codes.push_back(strtol(curCode.c_str(), nullptr, 16));
}
return codes;
}
void CheatManager::addGameSharkCheat(const std::string& name, const std::string& s)
{
std::vector<u32> codes = parseCodes(s);
Cheat conditionCheat;
unsigned conditionLimit = 0;
for (unsigned i = 0; i < codes.size(); i++)
{
if (i < conditionLimit)
cheats.push_back(conditionCheat);
Cheat cheat{};
cheat.description = name;
u32 code = (codes[i] & 0xff000000) >> 24;
switch (code)
{
case 0:
case 1:
case 2:
{
// 8/16/32-bit write
if (i + 1 >= codes.size())
throw FlycastException("Missing value");
cheat.type = Cheat::Type::setValue;
cheat.size = code == 0 ? 8 : code == 1 ? 16 : 32;
cheat.address = codes[i] & 0x00ffffff;
cheat.value = codes[++i];
cheats.push_back(cheat);
}
break;
case 3:
{
u32 subcode = (codes[i] & 0x00ff0000) >> 16;
switch (subcode)
{
case 0:
{
// Group write
int count = codes[i] & 0xffff;
if (i + count + 1 >= codes.size())
throw FlycastException("Missing values");
cheat.type = Cheat::Type::setValue;
cheat.size = 32;
cheat.address = codes[++i] & 0x00ffffff;
for (int j = 0; j < count; j++)
{
if (j == 1)
cheat.description += " (cont'd)";
cheat.value = codes[++i];
cheats.push_back(cheat);
cheat.address += 4;
if (j < count - 1 && i < conditionLimit)
cheats.push_back(conditionCheat);
}
}
break;
case 1:
case 2:
{
// 8-bit inc/decrement
if (i + 1 >= codes.size())
throw FlycastException("Missing value");
cheat.type = subcode == 1 ? Cheat::Type::increase : Cheat::Type::decrease;
cheat.size = 8;
cheat.value = codes[i] & 0xff;
cheat.address = codes[++i] & 0x00ffffff;
cheats.push_back(cheat);
}
break;
case 3:
case 4:
{
// 16-bit inc/decrement
if (i + 1 >= codes.size())
throw FlycastException("Missing value");
cheat.type = subcode == 3 ? Cheat::Type::increase : Cheat::Type::decrease;
cheat.size = 16;
cheat.value = codes[i] & 0xffff;
cheat.address = codes[++i] & 0x00ffffff;
cheats.push_back(cheat);
}
break;
case 5:
case 6:
{
// 32-bit inc/decrement
if (i + 2 >= codes.size())
throw FlycastException("Missing address or value");
cheat.type = subcode == 5 ? Cheat::Type::increase : Cheat::Type::decrease;
cheat.size = 32;
cheat.address = codes[++i] & 0x00ffffff;
cheat.value = codes[++i];
cheats.push_back(cheat);
}
break;
default:
throw FlycastException("Unsupported cheat type");
}
}
break;
case 4:
{
// 32-bit repeat write
if (i + 2 >= codes.size())
throw FlycastException("Missing count or value");
cheat.type = Cheat::Type::setValue;
cheat.size = 32;
cheat.address = codes[i] & 0x00ffffff;
cheat.repeatCount = codes[++i] >> 16;
cheat.repeatAddressIncrement = codes[i] & 0xffff;
cheat.value = codes[++i];
cheats.push_back(cheat);
}
break;
case 5:
{
// copy bytes
if (i + 2 >= codes.size())
throw FlycastException("Missing count or destination address");
cheat.type = Cheat::Type::copy;
cheat.size = 8;
cheat.address = codes[i] & 0x00ffffff;
cheat.destAddress = codes[++i] & 0x00ffffff;
cheat.repeatCount = codes[++i];
cheats.push_back(cheat);
}
break;
// TODO 7 change decryption type
// TODO 0xb delay applying codes
// TODO 0xc global enable test
case 0xd:
{
// enable next code if eq/neq/lt/gt
if (i + 1 >= codes.size())
throw FlycastException("Missing count or destination address");
cheat.size = 16;
cheat.address = codes[i] & 0x00ffffff;
switch (codes[++i] >> 16)
{
case 0:
cheat.type = Cheat::Type::runNextIfEq;
break;
case 1:
cheat.type = Cheat::Type::runNextIfNeq;
break;
case 2:
cheat.type = Cheat::Type::runNextIfLt;
break;
case 3:
cheat.type = Cheat::Type::runNextIfGt;
break;
default:
throw FlycastException("Unsupported conditional code");
}
cheat.value = codes[i] & 0xffff;
cheats.push_back(cheat);
}
break;
case 0xe:
{
// multiline enable codes if eq/neq/lt/gt
if (i + 1 >= codes.size())
throw FlycastException("Missing test address");
cheat.size = 16;
cheat.value = codes[i] & 0xffff;
conditionLimit = i + 1 + ((codes[i] >> 16) & 0xff);
switch (codes[++i] >> 24)
{
case 0:
cheat.type = Cheat::Type::runNextIfEq;
break;
case 1:
cheat.type = Cheat::Type::runNextIfNeq;
break;
case 2:
cheat.type = Cheat::Type::runNextIfLt;
break;
case 3:
cheat.type = Cheat::Type::runNextIfGt;
break;
default:
throw FlycastException("Unsupported conditional code");
}
cheat.address = codes[i] & 0x00ffffff;
conditionCheat = cheat;
}
break;
default:
throw FlycastException("Unsupported cheat type");
}
}
active = !cheats.empty();
#ifndef LIBRETRO
std::string path = cfgLoadStr("cheats", gameId, "");
if (path == "")
{
path = get_game_save_prefix() + ".cht";
cfgSaveStr("cheats", gameId, path);
}
saveCheatFile(path);
#endif
}
void CheatManager::saveCheatFile(const std::string& filename)
{
#ifndef LIBRETRO
emucfg::ConfigFile cfg;
cfg.set_int("", "cheats", cheats.size());
int i = 0;
for (const Cheat& cheat : cheats)
{
std::string prefix = "cheat" + std::to_string(i) + "_";
cfg.set_int("", prefix + "address", cheat.address);
cfg.set_int("", prefix + "address_bit_position", 0); // FIXME
cfg.set_bool("", prefix + "big_endian", false);
cfg.set_int("", prefix + "cheat_type", (int)cheat.type);
cfg.set("", prefix + "code", "");
cfg.set("", prefix + "desc", cheat.description);
cfg.set_int("", prefix + "dest_address", cheat.destAddress);
cfg.set_bool("", prefix + "enable", false); // force all cheats disabled at start
cfg.set_int("", prefix + "handler", 1);
int memSize;
switch (cheat.size) {
case 1:
memSize = 0;
break;
case 2:
memSize = 1;
break;
case 4:
memSize = 2;
break;
case 8:
memSize = 3;
break;
case 16:
memSize = 4;
break;
case 32:
default:
memSize = 5;
break;
}
cfg.set_int("", prefix + "memory_search_size", memSize);
cfg.set_int("", prefix + "value", cheat.value);
cfg.set_int("", prefix + "repeat_count", cheat.repeatCount);
cfg.set_int("", prefix + "repeat_add_to_value", cheat.repeatValueIncrement);
cfg.set_int("", prefix + "repeat_add_to_address", cheat.repeatAddressIncrement);
i++;
}
FILE *fp = nowide::fopen(filename.c_str(), "w");
if (fp == nullptr)
throw FlycastException("Can't save cheat file");
cfg.save(fp);
fclose(fp);
#endif
}

View File

@ -39,7 +39,8 @@ struct Cheat
runNextIfEq,
runNextIfNeq,
runNextIfGt,
runNextIfLt
runNextIfLt,
copy
};
Type type = Type::disabled;
std::string description;
@ -50,6 +51,7 @@ struct Cheat
u32 repeatCount = 1;
u32 repeatValueIncrement = 0;
u32 repeatAddressIncrement = 0;
u32 destAddress = 0;
};
class CheatManager
@ -62,8 +64,10 @@ public:
bool cheatEnabled(size_t index) const { return cheats[index].enabled; }
void enableCheat(size_t index, bool enabled) { cheats[index].enabled = enabled; }
void loadCheatFile(const std::string& filename);
void saveCheatFile(const std::string& filename);
// Returns true if using 16:9 anamorphic screen ratio
bool isWidescreen() const { return widescreen_cheat != nullptr; }
void addGameSharkCheat(const std::string& name, const std::string& s);
private:
u32 readRam(u32 addr, u32 bits);
@ -75,6 +79,10 @@ private:
bool active = false;
std::vector<Cheat> cheats;
std::string gameId;
friend class CheatManagerTest_TestLoad_Test;
friend class CheatManagerTest_TestGameShark_Test;
friend class CheatManagerTest_TestSave_Test;
};
extern CheatManager cheatManager;

View File

@ -442,7 +442,7 @@ void gui_stop_game(const std::string& message)
game_started = false;
reset_vmus();
if (!message.empty())
error_msg = "Flycast has stopped.\n\n" + message;
gui_error("Flycast has stopped.\n\n" + message);
}
else
{
@ -2105,7 +2105,7 @@ static void gui_display_content()
DiscSwap(game.path);
gui_state = GuiState::Closed;
} catch (const FlycastException& e) {
error_msg = e.what();
gui_error(e.what());
}
}
else
@ -2131,7 +2131,6 @@ static void gui_display_content()
ImGui::PopStyleVar();
ImGui::PopStyleVar();
error_popup();
contentpath_warning_popup();
}
@ -2206,7 +2205,7 @@ static void gui_network_start()
NetworkHandshake::instance->stop();
gui_state = GuiState::Main;
emu.unloadGame();
error_msg = e.what();
gui_error(e.what());
}
}
else
@ -2283,7 +2282,7 @@ static void gui_display_loadscreen()
}
} catch (const FlycastException& ex) {
ERROR_LOG(BOOT, "%s", ex.what());
error_msg = ex.what();
gui_error(ex.what());
#ifdef TEST_AUTOMATION
die("Game load failed");
#endif
@ -2354,6 +2353,7 @@ void gui_display_ui()
die("Unknown UI state");
break;
}
error_popup();
ImGui::Render();
ImGui_impl_RenderDrawData(ImGui::GetDrawData());
@ -2615,3 +2615,8 @@ static void term_vmus()
crosshairTexId = ImTextureID();
}
}
void gui_error(const std::string& what)
{
error_msg = what;
}

View File

@ -39,6 +39,7 @@ void gui_set_mouse_wheel(float delta);
void gui_set_insets(int left, int right, int top, int bottom);
void gui_stop_game(const std::string& message = "");
void gui_start_game(const std::string& path);
void gui_error(const std::string& what);
extern int screen_dpi;
extern float scaling;

View File

@ -21,8 +21,60 @@
#include "gui_util.h"
#include "cheats.h"
static bool addingCheat;
static void addCheat()
{
static char cheatName[64];
static char cheatCode[128];
centerNextWindow();
ImGui::SetNextWindowSize(ImVec2(std::min(ImGui::GetIO().DisplaySize.x, 600 * scaling), std::min(ImGui::GetIO().DisplaySize.y, 400 * scaling)));
ImGui::Begin("##main", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar
| ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize);
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(20 * scaling, 8 * scaling)); // from 8, 4
ImGui::AlignTextToFramePadding();
ImGui::Indent(10 * scaling);
ImGui::Text("ADD CHEAT");
ImGui::SameLine(ImGui::GetWindowContentRegionMax().x - ImGui::CalcTextSize("Cancel").x - ImGui::GetStyle().FramePadding.x * 4.f
- ImGui::CalcTextSize("OK").x - ImGui::GetStyle().ItemSpacing.x);
if (ImGui::Button("Cancel"))
addingCheat = false;
ImGui::SameLine();
if (ImGui::Button("OK"))
{
try {
cheatManager.addGameSharkCheat(cheatName, cheatCode);
addingCheat = false;
cheatName[0] = 0;
cheatCode[0] = 0;
} catch (const FlycastException& e) {
gui_error(e.what());
}
}
ImGui::Unindent(10 * scaling);
ImGui::PopStyleVar();
ImGui::BeginChild(ImGui::GetID("input"), ImVec2(0, 0), true);
{
ImGui::InputText("Name", cheatName, sizeof(cheatName), 0, nullptr, nullptr);
ImGui::Text("Code:");
ImGui::InputTextMultiline("Code", cheatCode, sizeof(cheatCode), ImVec2(-FLT_MIN, ImGui::GetTextLineHeight() * 8), 0, nullptr, nullptr);
}
ImGui::EndChild();
ImGui::End();
}
void gui_cheats()
{
if (addingCheat)
{
addCheat();
return;
}
centerNextWindow();
ImGui::SetNextWindowSize(ImVec2(std::min(ImGui::GetIO().DisplaySize.x, 600 * scaling), std::min(ImGui::GetIO().DisplaySize.y, 400 * scaling)));
@ -34,8 +86,11 @@ void gui_cheats()
ImGui::Indent(10 * scaling);
ImGui::Text("CHEATS");
ImGui::SameLine(ImGui::GetWindowContentRegionMax().x - ImGui::CalcTextSize("Close").x - ImGui::GetStyle().FramePadding.x * 4.f
- ImGui::CalcTextSize("Load").x - ImGui::GetStyle().ItemSpacing.x);
ImGui::SameLine(ImGui::GetWindowContentRegionMax().x - ImGui::CalcTextSize("Add").x - ImGui::CalcTextSize("Close").x - ImGui::GetStyle().FramePadding.x * 6.f
- ImGui::CalcTextSize("Load").x - ImGui::GetStyle().ItemSpacing.x * 2);
if (ImGui::Button("Add"))
addingCheat = true;
ImGui::SameLine();
if (ImGui::Button("Load"))
ImGui::OpenPopup("Select cheat file");
select_file_popup("Select cheat file", [](bool cancelled, std::string selection)

View File

@ -128,3 +128,13 @@ static inline std::string trim_trailing_ws(const std::string& str,
return str.substr(0, strEnd + 1);
}
static inline std::string trim_ws(const std::string& str,
const std::string& whitespace = " ")
{
const auto strStart = str.find_first_not_of(whitespace);
if (strStart == std::string::npos)
return "";
return str.substr(strStart, str.find_last_not_of(whitespace) + 1 - strStart);
}

View File

@ -0,0 +1,255 @@
#include "gtest/gtest.h"
#include "types.h"
#include "cfg/ini.h"
#include "cfg/cfg.h"
#include "cheats.h"
#include "emulator.h"
#include "log/LogManager.h"
class CheatManagerTest : public ::testing::Test {
protected:
void SetUp() override {
emu.init();
LogManager::Init();
}
};
TEST_F(CheatManagerTest, TestLoad)
{
FILE *fp = fopen("test.cht", "w");
const char *s = R"(
cheat0_address = "1234"
cheat0_address_bit_position = "0"
cheat0_big_endian = "false"
cheat0_cheat_type = "1"
cheat0_code = ""
cheat0_desc = "widescreen"
cheat0_dest_address = "7890"
cheat0_enable = "false"
cheat0_handler = "1"
cheat0_memory_search_size = "5"
cheat0_repeat_add_to_address = "4"
cheat0_repeat_add_to_value = "16"
cheat0_repeat_count = "3"
cheat0_value = "5678"
cheats = "1"
)";
fputs(s, fp);
fclose(fp);
CheatManager mgr;
mgr.reset("TESTGAME");
mgr.loadCheatFile("test.cht");
ASSERT_EQ(1, (int)mgr.cheatCount());
ASSERT_EQ("widescreen", mgr.cheatDescription(0));
ASSERT_FALSE(mgr.cheatEnabled(0));
mgr.enableCheat(0, true);
ASSERT_TRUE(mgr.cheatEnabled(0));
mgr.enableCheat(0, false);
ASSERT_FALSE(mgr.cheatEnabled(0));
ASSERT_EQ(1234u, mgr.cheats[0].address);
ASSERT_EQ(5678u, mgr.cheats[0].value);
ASSERT_EQ(Cheat::Type::setValue, mgr.cheats[0].type);
ASSERT_EQ(32u, mgr.cheats[0].size);
ASSERT_EQ(3u, mgr.cheats[0].repeatCount);
ASSERT_EQ(4u, mgr.cheats[0].repeatAddressIncrement);
ASSERT_EQ(16u, mgr.cheats[0].repeatValueIncrement);
ASSERT_EQ(7890u, mgr.cheats[0].destAddress);
}
TEST_F(CheatManagerTest, TestGameShark)
{
CheatManager mgr;
mgr.reset("TESTGS1");
mgr.addGameSharkCheat("cheat1", "00123456 deadc0d3");
ASSERT_EQ("cheat1", mgr.cheats[0].description);
ASSERT_EQ(Cheat::Type::setValue, mgr.cheats[0].type);
ASSERT_EQ(0x123456u, mgr.cheats[0].address);
ASSERT_EQ(0xdeadc0d3u, mgr.cheats[0].value);
ASSERT_EQ(8u, mgr.cheats[0].size);
ASSERT_EQ(1u, mgr.cheats[0].repeatCount);
mgr.reset("TESTGS2");
mgr.addGameSharkCheat("cheat2", " 01222222\ndeadc0d3 ");
ASSERT_EQ(Cheat::Type::setValue, mgr.cheats[0].type);
ASSERT_EQ(0x222222u, mgr.cheats[0].address);
ASSERT_EQ(0xdeadc0d3u, mgr.cheats[0].value);
ASSERT_EQ(16u, mgr.cheats[0].size);
ASSERT_EQ(1u, mgr.cheats[0].repeatCount);
mgr.reset("TESTGS3");
mgr.addGameSharkCheat("cheat3", "\n02333333 \t baadf00d\n");
ASSERT_EQ(Cheat::Type::setValue, mgr.cheats[0].type);
ASSERT_EQ(0x333333u, mgr.cheats[0].address);
ASSERT_EQ(0xbaadf00du, mgr.cheats[0].value);
ASSERT_EQ(32u, mgr.cheats[0].size);
ASSERT_EQ(1u, mgr.cheats[0].repeatCount);
mgr.reset("TESTGS4");
mgr.addGameSharkCheat("cheat4", "\n03010042 \t 0c444444\n");
ASSERT_EQ(Cheat::Type::increase, mgr.cheats[0].type);
ASSERT_EQ(0x444444u, mgr.cheats[0].address);
ASSERT_EQ(0x42u, mgr.cheats[0].value);
ASSERT_EQ(8u, mgr.cheats[0].size);
ASSERT_EQ(1u, mgr.cheats[0].repeatCount);
mgr.reset("TESTGS5");
mgr.addGameSharkCheat("cheat5", "\n03041984 \t 0c555555\n");
ASSERT_EQ(Cheat::Type::decrease, mgr.cheats[0].type);
ASSERT_EQ(0x555555u, mgr.cheats[0].address);
ASSERT_EQ(0x1984u, mgr.cheats[0].value);
ASSERT_EQ(16u, mgr.cheats[0].size);
ASSERT_EQ(1u, mgr.cheats[0].repeatCount);
mgr.reset("TESTGS6");
mgr.addGameSharkCheat("cheat6", "03000003 0c666666 11111111 22222222 33333333");
ASSERT_EQ(3u, mgr.cheats.size());
ASSERT_EQ(Cheat::Type::setValue, mgr.cheats[0].type);
ASSERT_EQ(0x666666u, mgr.cheats[0].address);
ASSERT_EQ(0x11111111u, mgr.cheats[0].value);
ASSERT_EQ(32u, mgr.cheats[0].size);
ASSERT_EQ(1u, mgr.cheats[0].repeatCount);
ASSERT_EQ(Cheat::Type::setValue, mgr.cheats[1].type);
ASSERT_EQ(0x66666au, mgr.cheats[1].address);
ASSERT_EQ(0x22222222u, mgr.cheats[1].value);
ASSERT_EQ(32u, mgr.cheats[1].size);
ASSERT_EQ(1u, mgr.cheats[1].repeatCount);
ASSERT_EQ(Cheat::Type::setValue, mgr.cheats[2].type);
ASSERT_EQ(0x66666eu, mgr.cheats[2].address);
ASSERT_EQ(0x33333333u, mgr.cheats[2].value);
ASSERT_EQ(32u, mgr.cheats[0].size);
ASSERT_EQ(1u, mgr.cheats[0].repeatCount);
mgr.reset("TESTGS7");
mgr.addGameSharkCheat("cheat7", "04aaaaaa 00020002 11111111");
ASSERT_EQ(Cheat::Type::setValue, mgr.cheats[0].type);
ASSERT_EQ(0xaaaaaau, mgr.cheats[0].address);
ASSERT_EQ(0x11111111u, mgr.cheats[0].value);
ASSERT_EQ(32u, mgr.cheats[0].size);
ASSERT_EQ(2u, mgr.cheats[0].repeatCount);
ASSERT_EQ(2u, mgr.cheats[0].repeatAddressIncrement);
ASSERT_EQ(0u, mgr.cheats[0].repeatValueIncrement);
mgr.reset("TESTGS8");
mgr.addGameSharkCheat("cheat8", "05bbbbbb 8ccccccc 00000010");
ASSERT_EQ(Cheat::Type::copy, mgr.cheats[0].type);
ASSERT_EQ(0xbbbbbbu, mgr.cheats[0].address);
ASSERT_EQ(8u, mgr.cheats[0].size);
ASSERT_EQ(0x10u, mgr.cheats[0].repeatCount);
ASSERT_EQ(0xccccccu, mgr.cheats[0].destAddress);
mgr.reset("TESTGS9");
mgr.addGameSharkCheat("cheat9", "0d123456 0000ffff");
ASSERT_EQ(Cheat::Type::runNextIfEq, mgr.cheats[0].type);
ASSERT_EQ(0x123456u, mgr.cheats[0].address);
ASSERT_EQ(0xffffu, mgr.cheats[0].value);
ASSERT_EQ(16u, mgr.cheats[0].size);
ASSERT_EQ(1u, mgr.cheats[0].repeatCount);
mgr.reset("TESTGS10");
mgr.addGameSharkCheat("cheat10", "0d654321" "00031984");
ASSERT_EQ(Cheat::Type::runNextIfGt, mgr.cheats[0].type);
ASSERT_EQ(0x654321u, mgr.cheats[0].address);
ASSERT_EQ(0x1984u, mgr.cheats[0].value);
ASSERT_EQ(16u, mgr.cheats[0].size);
ASSERT_EQ(1u, mgr.cheats[0].repeatCount);
mgr.reset("TESTGS11");
mgr.addGameSharkCheat("cheat11", "0e021111 00123456 02222222 22222222 01111111 00001111");
ASSERT_EQ(Cheat::Type::runNextIfEq, mgr.cheats[0].type);
ASSERT_EQ(0x123456u, mgr.cheats[0].address);
ASSERT_EQ(0x1111u, mgr.cheats[0].value);
ASSERT_EQ(16u, mgr.cheats[0].size);
ASSERT_EQ(1u, mgr.cheats[0].repeatCount);
ASSERT_EQ(Cheat::Type::setValue, mgr.cheats[1].type);
ASSERT_EQ(0x222222u, mgr.cheats[1].address);
ASSERT_EQ(0x22222222u, mgr.cheats[1].value);
ASSERT_EQ(32u, mgr.cheats[1].size);
ASSERT_EQ(1u, mgr.cheats[1].repeatCount);
ASSERT_EQ(Cheat::Type::setValue, mgr.cheats[2].type);
ASSERT_EQ(0x111111u, mgr.cheats[2].address);
ASSERT_EQ(0x1111u, mgr.cheats[2].value);
ASSERT_EQ(16u, mgr.cheats[2].size);
ASSERT_EQ(1u, mgr.cheats[2].repeatCount);
}
TEST_F(CheatManagerTest, TestGameSharkError)
{
CheatManager mgr;
EXPECT_THROW(mgr.addGameSharkCheat("error", "00"), FlycastException);
EXPECT_THROW(mgr.addGameSharkCheat("error", "001234560000000"), FlycastException);
EXPECT_THROW(mgr.addGameSharkCheat("error", "00 123456 00000000"), FlycastException);
EXPECT_THROW(mgr.addGameSharkCheat("error", "00123456 "), FlycastException);
EXPECT_THROW(mgr.addGameSharkCheat("error", " 01123456 "), FlycastException);
EXPECT_THROW(mgr.addGameSharkCheat("error", " 02123456 "), FlycastException);
EXPECT_THROW(mgr.addGameSharkCheat("error", " 03000002 00123456 00000001"), FlycastException);
EXPECT_THROW(mgr.addGameSharkCheat("error", " 03013456"), FlycastException);
EXPECT_THROW(mgr.addGameSharkCheat("error", " 03023456"), FlycastException);
EXPECT_THROW(mgr.addGameSharkCheat("error", " 03033456"), FlycastException);
EXPECT_THROW(mgr.addGameSharkCheat("error", " 03043456"), FlycastException);
EXPECT_THROW(mgr.addGameSharkCheat("error", " 03050000 8c123456"), FlycastException);
EXPECT_THROW(mgr.addGameSharkCheat("error", " 03060000 8c123456"), FlycastException);
EXPECT_THROW(mgr.addGameSharkCheat("error", " 04654321 00030004"), FlycastException);
EXPECT_THROW(mgr.addGameSharkCheat("error", " 05654321 8c111111"), FlycastException);
EXPECT_THROW(mgr.addGameSharkCheat("error", " 0d0FFFFF"), FlycastException);
EXPECT_THROW(mgr.addGameSharkCheat("error", " 0e0FFFFF"), FlycastException);
}
TEST_F(CheatManagerTest, TestSave)
{
CheatManager mgr;
mgr.reset("TESTSAVE");
mgr.addGameSharkCheat("cheat1", "00010000 000000d3");
mgr.addGameSharkCheat("cheat2", "01020000 0000c0d3");
mgr.addGameSharkCheat("cheat3", "02030000 deadc0d3");
mgr.addGameSharkCheat("cheat4", "03000004 8c040000 00000001 00000002 00000003 00000004");
mgr.addGameSharkCheat("cheat5", "030100ff 8c050000");
mgr.addGameSharkCheat("cheat6", "0304ffff 8c060000");
mgr.loadCheatFile(cfgLoadStr("cheats", "TESTSAVE", ""));
ASSERT_EQ(Cheat::Type::setValue, mgr.cheats[0].type);
ASSERT_EQ(0x010000u, mgr.cheats[0].address);
ASSERT_EQ(0xd3u, mgr.cheats[0].value);
ASSERT_EQ(8u, mgr.cheats[0].size);
ASSERT_EQ(1u, mgr.cheats[0].repeatCount);
ASSERT_EQ(Cheat::Type::setValue, mgr.cheats[1].type);
ASSERT_EQ(0x020000u, mgr.cheats[1].address);
ASSERT_EQ(0xc0d3u, mgr.cheats[1].value);
ASSERT_EQ(16u, mgr.cheats[1].size);
ASSERT_EQ(Cheat::Type::setValue, mgr.cheats[2].type);
ASSERT_EQ(0x030000u, mgr.cheats[2].address);
ASSERT_EQ(0xdeadc0d3u, mgr.cheats[2].value);
ASSERT_EQ(32u, mgr.cheats[2].size);
ASSERT_EQ(Cheat::Type::setValue, mgr.cheats[3].type);
ASSERT_EQ(0x040000u, mgr.cheats[3].address);
ASSERT_EQ(1u, mgr.cheats[3].value);
ASSERT_EQ(32u, mgr.cheats[3].size);
ASSERT_EQ(Cheat::Type::setValue, mgr.cheats[4].type);
ASSERT_EQ(0x040004u, mgr.cheats[4].address);
ASSERT_EQ(2u, mgr.cheats[4].value);
ASSERT_EQ(32u, mgr.cheats[4].size);
ASSERT_EQ(Cheat::Type::setValue, mgr.cheats[5].type);
ASSERT_EQ(0x040008u, mgr.cheats[5].address);
ASSERT_EQ(3u, mgr.cheats[5].value);
ASSERT_EQ(32u, mgr.cheats[5].size);
ASSERT_EQ(Cheat::Type::setValue, mgr.cheats[6].type);
ASSERT_EQ(0x04000cu, mgr.cheats[6].address);
ASSERT_EQ(4u, mgr.cheats[6].value);
ASSERT_EQ(32u, mgr.cheats[6].size);
ASSERT_EQ(Cheat::Type::increase, mgr.cheats[7].type);
ASSERT_EQ(0x050000u, mgr.cheats[7].address);
ASSERT_EQ(0xffu, mgr.cheats[7].value);
ASSERT_EQ(8u, mgr.cheats[7].size);
ASSERT_EQ(Cheat::Type::decrease, mgr.cheats[8].type);
ASSERT_EQ(0x060000u, mgr.cheats[8].address);
ASSERT_EQ(0xffffu, mgr.cheats[8].value);
ASSERT_EQ(16u, mgr.cheats[8].size);
}

View File

@ -0,0 +1,101 @@
#include "gtest/gtest.h"
#include "types.h"
#include "cfg/ini.h"
class ConfigFileTest : public ::testing::Test {
};
TEST_F(ConfigFileTest, TestLoadSave)
{
using namespace emucfg;
ConfigFile file;
file.set("", "prop1", "value1");
file.set_int("", "prop2", 2);
file.set_bool("", "prop3", true);
ASSERT_EQ("value1", file.get("", "prop1", ""));
ASSERT_EQ(2, file.get_int("", "prop2", 0));
ASSERT_TRUE(file.get_bool("", "prop3", false));
FILE *fp = fopen("test.cfg", "w");
file.save(fp);
fclose(fp);
fp = fopen("test.cfg", "r");
char buf[1024];
int l = fread(buf, 1, sizeof(buf) - 1, fp);
buf[l] = '\0';
fclose(fp);
ASSERT_EQ("prop1 = value1\nprop2 = 2\nprop3 = yes\n", std::string(buf));
fp = fopen("test.cfg", "r");
file = {};
file.parse(fp);
fclose(fp);
ASSERT_EQ("value1", file.get("", "prop1", ""));
ASSERT_EQ(2, file.get_int("", "prop2", 0));
ASSERT_TRUE(file.get_bool("", "prop3", false));
}
TEST_F(ConfigFileTest, TestQuotes)
{
using namespace emucfg;
FILE *fp = fopen("test.cfg", "w");
fprintf(fp, "propWithQuotes=\"value with quotes\"\n");
fprintf(fp, "propWithQuotes2=\"42\"\n");
fprintf(fp, "propWithQuotes3=\"true\"\n");
fclose(fp);
fp = fopen("test.cfg", "r");
ConfigFile file;
file.parse(fp);
fclose(fp);
ASSERT_EQ("value with quotes", file.get("", "propWithQuotes", ""));
ASSERT_EQ(42, file.get_int("", "propWithQuotes2", 0));
ASSERT_TRUE(file.get_bool("", "propWithQuotes3", false));
}
TEST_F(ConfigFileTest, TestTrim)
{
using namespace emucfg;
FILE *fp = fopen("test.cfg", "w");
fprintf(fp, " prop = \"value 1 \" \n\n\n");
fprintf(fp, " prop2 = 42 \n");
fprintf(fp, " prop3 = yes \r\n\n");
fclose(fp);
fp = fopen("test.cfg", "r");
ConfigFile file;
file.parse(fp);
fclose(fp);
ASSERT_EQ("value 1 ", file.get("", "prop", ""));
ASSERT_EQ(42, file.get_int("", "prop2", 0));
ASSERT_TRUE(file.get_bool("", "prop3", false));
}
TEST_F(ConfigFileTest, TestLoadSaveSection)
{
using namespace emucfg;
ConfigFile file;
file.set("sect1", "prop1", "value1");
file.set_int("sect2", "prop2", 2);
file.set_bool("sect2", "prop3", true);
ASSERT_EQ("value1", file.get("sect1", "prop1", ""));
ASSERT_EQ(2, file.get_int("sect2", "prop2", 0));
ASSERT_TRUE(file.get_bool("sect2", "prop3", false));
FILE *fp = fopen("test.cfg", "w");
file.save(fp);
fclose(fp);
fp = fopen("test.cfg", "r");
char buf[1024];
int l = fread(buf, 1, sizeof(buf) - 1, fp);
buf[l] = '\0';
fclose(fp);
ASSERT_EQ("[sect1]\nprop1 = value1\n[sect2]\nprop2 = 2\nprop3 = yes\n", std::string(buf));
fp = fopen("test.cfg", "r");
file = {};
file.parse(fp);
fclose(fp);
ASSERT_EQ("value1", file.get("sect1", "prop1", ""));
ASSERT_EQ(2, file.get_int("sect2", "prop2", 0));
ASSERT_TRUE(file.get_bool("sect2", "prop3", false));
}