// Copyright 2010 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "Core/GeckoCodeConfig.h" #include #include #include #include #include #include "Common/HttpRequest.h" #include "Common/IniFile.h" #include "Common/Logging/Log.h" #include "Common/StringUtil.h" #include "Core/CheatCodes.h" namespace Gecko { std::vector DownloadCodes(std::string gametdb_id, bool* succeeded, bool use_https) { // TODO: Fix https://bugs.dolphin-emu.org/issues/11772 so we don't need this workaround const std::string protocol = use_https ? "https://" : "http://"; // codes.rc24.xyz is a mirror of the now defunct geckocodes.org. std::string endpoint{protocol + "codes.rc24.xyz/txt.php?txt=" + gametdb_id}; Common::HttpRequest http; // The server always redirects once to the same location. http.FollowRedirects(1); const Common::HttpRequest::Response response = http.Get(endpoint); *succeeded = response.has_value(); if (!response) return {}; // temp vector containing parsed codes std::vector gcodes; // parse the codes std::istringstream ss(std::string(response->begin(), response->end())); std::string line; // seek past the header, get to the first code std::getline(ss, line); std::getline(ss, line); std::getline(ss, line); int read_state = 0; GeckoCode gcode; while ((std::getline(ss, line).good())) { // Remove \r at the end of the line for files using windows line endings, std::getline only // removes \n line = StripWhitespace(line); if (line.empty()) { // add the code if (!gcode.codes.empty()) gcodes.push_back(gcode); gcode = GeckoCode(); read_state = 0; continue; } switch (read_state) { // read new code case 0: { std::istringstream ssline(line); // stop at [ character (beginning of contributor name) std::getline(ssline, gcode.name, '['); gcode.name = StripWhitespace(gcode.name); gcode.user_defined = true; // read the code creator name std::getline(ssline, gcode.creator, ']'); read_state = 1; } break; // read code lines case 1: { std::istringstream ssline(line); std::string addr, data; // Some locales (e.g. fr_FR.UTF-8) don't split the string stream on space // Use the C locale to workaround this behavior ssline.imbue(std::locale::classic()); ssline >> addr >> data; ssline.seekg(0); // check if this line a code, silly, but the dumb txt file comment lines can start with // valid hex chars :/ if (8 == addr.length() && 8 == data.length()) { GeckoCode::Code new_code; new_code.original_line = line; ssline >> std::hex >> new_code.address >> new_code.data; gcode.codes.push_back(new_code); } else { gcode.notes.push_back(line); read_state = 2; // start reading comments } } break; // read comment lines case 2: // append comment line gcode.notes.push_back(line); break; } } // add the last code if (!gcode.codes.empty()) gcodes.push_back(gcode); return gcodes; } std::vector LoadCodes(const IniFile& globalIni, const IniFile& localIni) { std::vector gcodes; for (const IniFile* ini : {&globalIni, &localIni}) { std::vector lines; ini->GetLines("Gecko", &lines, false); GeckoCode gcode; lines.erase(std::remove_if(lines.begin(), lines.end(), [](const auto& line) { return line.empty() || line[0] == '#'; }), lines.end()); for (auto& line : lines) { std::istringstream ss(line); // Some locales (e.g. fr_FR.UTF-8) don't split the string stream on space // Use the C locale to workaround this behavior ss.imbue(std::locale::classic()); switch ((line)[0]) { // enabled or disabled code case '+': ss.seekg(1); [[fallthrough]]; case '$': if (!gcode.name.empty()) gcodes.push_back(gcode); gcode = GeckoCode(); gcode.enabled = (1 == ss.tellg()); // silly gcode.user_defined = (ini == &localIni); ss.seekg(1, std::ios_base::cur); // read the code name std::getline(ss, gcode.name, '['); // stop at [ character (beginning of contributor name) gcode.name = StripWhitespace(gcode.name); // read the code creator name std::getline(ss, gcode.creator, ']'); break; // notes case '*': gcode.notes.push_back(std::string(++line.begin(), line.end())); break; // either part of the code, or an option choice default: { GeckoCode::Code new_code; // TODO: support options if (std::optional code = DeserializeLine(line)) new_code = *code; else new_code.original_line = line; gcode.codes.push_back(new_code); } break; } } // add the last code if (!gcode.name.empty()) { gcodes.push_back(gcode); } ReadEnabledAndDisabled(*ini, "Gecko", &gcodes); if (ini == &globalIni) { for (GeckoCode& code : gcodes) code.default_enabled = code.enabled; } } return gcodes; } static std::string MakeGeckoCodeTitle(const GeckoCode& code) { std::string title = '$' + code.name; if (!code.creator.empty()) { title += " [" + code.creator + ']'; } return title; } // used by the SaveGeckoCodes function static void SaveGeckoCode(std::vector& lines, const GeckoCode& gcode) { if (!gcode.user_defined) return; lines.push_back(MakeGeckoCodeTitle(gcode)); // save all the code lines for (const GeckoCode::Code& code : gcode.codes) { lines.push_back(code.original_line); } // save the notes for (const std::string& note : gcode.notes) lines.push_back('*' + note); } void SaveCodes(IniFile& inifile, const std::vector& gcodes) { std::vector lines; std::vector enabled_lines; std::vector disabled_lines; for (const GeckoCode& geckoCode : gcodes) { if (geckoCode.enabled != geckoCode.default_enabled) (geckoCode.enabled ? enabled_lines : disabled_lines).emplace_back('$' + geckoCode.name); SaveGeckoCode(lines, geckoCode); } inifile.SetLines("Gecko", lines); inifile.SetLines("Gecko_Enabled", enabled_lines); inifile.SetLines("Gecko_Disabled", disabled_lines); } std::optional DeserializeLine(const std::string& line) { std::vector items = SplitString(line, ' '); GeckoCode::Code code; code.original_line = line; if (items.size() < 2) return std::nullopt; if (!TryParse(items[0], &code.address, 16)) return std::nullopt; if (!TryParse(items[1], &code.data, 16)) return std::nullopt; return code; } } // namespace Gecko