diff --git a/CMakeLists.txt b/CMakeLists.txt index eeb29c1a4..3a339dda6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/core/cfg/cl.cpp b/core/cfg/cl.cpp index a036cb415..0d8a874b2 100644 --- a/core/cfg/cl.cpp +++ b/core/cfg/cl.cpp @@ -1,8 +1,5 @@ /* Command line parsing - ~yay~ - - Nothing too interesting here, really */ #include @@ -10,39 +7,9 @@ #include #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; diff --git a/core/cfg/ini.cpp b/core/cfg/ini.cpp index c735ea663..2789079ed 100644 --- a/core/cfg/ini.cpp +++ b/core/cfg/ini.cpp @@ -1,8 +1,7 @@ #include "ini.h" #include "types.h" #include - -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); } } diff --git a/core/cheats.cpp b/core/cheats.cpp index a18b8b90e..04f7da2fd 100644 --- a/core/cheats.cpp +++ b/core/cheats.cpp @@ -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 parseCodes(const std::string& s) +{ + std::vector 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 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 +} diff --git a/core/cheats.h b/core/cheats.h index 042c90cc0..03a48b4c2 100644 --- a/core/cheats.h +++ b/core/cheats.h @@ -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 cheats; std::string gameId; + + friend class CheatManagerTest_TestLoad_Test; + friend class CheatManagerTest_TestGameShark_Test; + friend class CheatManagerTest_TestSave_Test; }; extern CheatManager cheatManager; diff --git a/core/rend/gui.cpp b/core/rend/gui.cpp index 93650bda9..17caa3178 100644 --- a/core/rend/gui.cpp +++ b/core/rend/gui.cpp @@ -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; +} diff --git a/core/rend/gui.h b/core/rend/gui.h index c468ee33f..132a4724d 100644 --- a/core/rend/gui.h +++ b/core/rend/gui.h @@ -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; diff --git a/core/rend/gui_cheats.cpp b/core/rend/gui_cheats.cpp index 78652b076..5d206d7fd 100644 --- a/core/rend/gui_cheats.cpp +++ b/core/rend/gui_cheats.cpp @@ -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) diff --git a/core/stdclass.h b/core/stdclass.h index 81daed3be..11d804df4 100644 --- a/core/stdclass.h +++ b/core/stdclass.h @@ -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); +} diff --git a/tests/src/CheatManagerTest.cpp b/tests/src/CheatManagerTest.cpp new file mode 100644 index 000000000..0064b90f3 --- /dev/null +++ b/tests/src/CheatManagerTest.cpp @@ -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); +} diff --git a/tests/src/ConfigFileTest.cpp b/tests/src/ConfigFileTest.cpp new file mode 100644 index 000000000..55c0eaa2a --- /dev/null +++ b/tests/src/ConfigFileTest.cpp @@ -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)); +} +