#include "internal.h" #include #include #include #include #include #include #include #include "avs/game.h" #include "build/resource.h" #include "cfg/configurator.h" #include "external/fmt/include/fmt/chrono.h" #include "external/rapidjson/document.h" #include "external/rapidjson/error/en.h" #include "external/rapidjson/prettywriter.h" #include "external/rapidjson/stringbuffer.h" #include "launcher/launcher.h" #include "util/fileutils.h" #include "util/libutils.h" #include "util/logging.h" #include "util/netutils.h" #include "util/resutils.h" #include "util/sigscan.h" #include "util/utils.h" using namespace rapidjson; namespace patcher { std::vector getExtraDlls(const std::string& firstDll) { if (!EXTRA_DLLS.contains(firstDll)) { return {}; } return EXTRA_DLLS[firstDll]; } // utility std::string displayPath(const std::filesystem::path &path) { return fmt::format(FMT_STRING("{}"), path); } std::string getFromUrl(const std::string& dll_name, const std::string& url) { log_info("patchmanager", "getting patches from URL: {}, for file: {}", url, dll_name); std::string result; auto components = URL_COMPONENTS {}; components.dwStructSize = sizeof(components); components.dwHostNameLength = -1; components.dwUrlPathLength = -1; auto wideUrl = std::wstring(url.begin(), url.end()); if (!WinHttpCrackUrl(wideUrl.c_str(), 0, 0, &components)) { const auto gle = GetLastError(); url_fetch_errors += fmt::format("WinHttpCrackUrl failed: {}\n", gle); log_warning("patchmanager", "WinHttpCrackUrl failed: {}", gle); return result; } auto session = WinHttpOpen(L"spice2x", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, nullptr, nullptr, 0); auto session_ = std::unique_ptr(session, WinHttpCloseHandle); if (!session) { const auto gle = GetLastError(); url_fetch_errors += fmt::format("WinHttpOpen failed: {}\n", gle); log_warning("patchmanager", "WinHttpOpen failed: {}", gle); return result; } auto hostname = std::wstring(components.lpszHostName, components.dwHostNameLength); auto connect = WinHttpConnect(session, hostname.c_str(), components.nPort, 0); auto connect_ = std::unique_ptr(connect, WinHttpCloseHandle); if (!connect) { const auto gle = GetLastError(); url_fetch_errors += fmt::format("WinHttpConnect failed: {}\n", gle); log_warning("patchmanager", "WinHttpConnect failed: {}", gle); return result; } auto flags = 0; if (components.nScheme == INTERNET_SCHEME_HTTPS) { flags = WINHTTP_FLAG_SECURE; } auto urlPath = std::wstring(components.lpszUrlPath, components.dwUrlPathLength); auto request = WinHttpOpenRequest(connect, L"GET", urlPath.c_str(), nullptr, nullptr, nullptr, flags); auto request_ = std::unique_ptr(request, WinHttpCloseHandle); if (!request) { const auto gle = GetLastError(); url_fetch_errors += fmt::format("WinHttpOpenRequest failed: {}\n", gle); log_warning("patchmanager", "WinHttpOpenRequest failed: {}", gle); return result; } if (!WinHttpSendRequest(request, nullptr, 0, nullptr, 0, 0, 0)) { const auto gle = GetLastError(); log_warning("patchmanager", "WinHttpSendRequest failed: {}", gle); url_fetch_errors += fmt::format("WinHttpSendRequest failed: {}\n", gle); if (gle == ERROR_WINHTTP_SECURE_FAILURE) { url_fetch_errors += "\nERROR_WINHTTP_SECURE_FAILURE\n\n"; url_fetch_errors += "Most likely TLS 1.1 / TLS 1.2 error on old OS versions.\n\n"; url_fetch_errors += "Look up MSDN article on 'Update to enable TLS 1.1 and TLS 1.2 as default secure protocols in WinHTTP in Windows' for a fix.\n"; } else if (gle == ERROR_WINHTTP_NAME_NOT_RESOLVED) { url_fetch_errors += "\nERROR_WINHTTP_NAME_NOT_RESOLVED\n\n"; url_fetch_errors += "The server name could not be resolved.\n\n"; url_fetch_errors += "Website is offline, or your internet connection is down."; } else if (gle == ERROR_WINHTTP_TIMEOUT) { url_fetch_errors += "\nERROR_WINHTTP_TIMEOUT\n\n"; url_fetch_errors += "The request timed out."; } return result; } if (!WinHttpReceiveResponse(request, nullptr)) { const auto gle = GetLastError(); log_warning("patchmanager", "WinHttpReceiveResponse failed: {}", gle); url_fetch_errors += fmt::format("WinHttpReceiveResponse failed: {}\n", gle); return result; } DWORD statusCode = 0; DWORD statusCodeSize = sizeof(statusCode); DWORD queryFlags = WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER; if (!WinHttpQueryHeaders(request, queryFlags, nullptr, &statusCode, &statusCodeSize, nullptr)) { const auto gle = GetLastError(); log_warning("patchmanager", "WinHttpQueryHeaders failed: {}", gle); url_fetch_errors += fmt::format("WinHttpQueryHeaders failed: {}\n", gle); return result; } if (statusCode != 200) { log_warning("patchmanager", "Failed to fetch URL: got unexpected status code {}", statusCode); url_fetch_errors += fmt::format( "{}: HTTP Status: {} ({})\n", dll_name, statusCode, netutils::http_status_reason_phrase(statusCode)); if (statusCode == 404) { url_fetch_errors += "(Patch server did not find any patches for this game version)\n"; } return result; } DWORD bytesRead = 0; std::vector buffer(4096); while (WinHttpReadData(request, buffer.data(), buffer.size(), &bytesRead)) { if (bytesRead == 0) { break; } result.append(buffer.data(), bytesRead); } return result; } std::string get_game_identifier(const std::filesystem::path& dll_path, bool print_info) { uint32_t time_date_stamp = 0; uint32_t address_of_entry_point = 0; bool result = get_pe_identifier(dll_path, &time_date_stamp, &address_of_entry_point); if (!result) { return ""; } // concatenate TimeDateStamp and AddressOfEntryPoint std::string identifier = fmt::format( "{}-{:x}_{:x}", avs::game::MODEL, time_date_stamp, address_of_entry_point); if (print_info) { const auto time = std::chrono::system_clock::from_time_t(time_date_stamp); log_info( "patchmanager", "file: {}, patch id: {}, build timestamp of dll: {:%Y-%m-%d %H:%M}", dll_path.has_filename() ? dll_path.filename() : dll_path, identifier, time); } return identifier; } void load_embedded_patches(bool apply_patches) { // load embedded patches from resources auto patches_json = resutil::load_file_string(IDR_PATCHES); // parse document Document doc; doc.Parse(patches_json.c_str()); // check parse error auto error = doc.GetParseError(); if (error) { log_warning("patchmanager", "embedded patches json file parse error: {}", static_cast(error)); } const auto group_definitions = parse_patch_group_definitions(doc); // iterate patches for (auto &patch : doc.GetArray()) { if (is_patch_group_definition(patch)) { continue; } // verfiy patch data auto name_it = patch.FindMember("name"); if (name_it == patch.MemberEnd() || !name_it->value.IsString()) { log_warning("patchmanager", "failed to parse patch name"); continue; } auto game_code_it = patch.FindMember("gameCode"); if (game_code_it == patch.MemberEnd() || !game_code_it->value.IsString()) { log_warning("patchmanager", "failed to parse game code for {}", name_it->value.GetString()); continue; } const std::string game_code( game_code_it->value.GetString(), game_code_it->value.GetStringLength()); auto description_it = patch.FindMember("description"); if (description_it == patch.MemberEnd() || !description_it->value.IsString()) { log_warning("patchmanager", "failed to parse description for {}", name_it->value.GetString()); continue; } auto type_it = patch.FindMember("type"); if (type_it == patch.MemberEnd() || !type_it->value.IsString()) { log_warning("patchmanager", "failed to parse type for {}", name_it->value.GetString()); continue; } auto preset_it = patch.FindMember("preset"); bool preset = false; if (preset_it != patch.MemberEnd() && preset_it->value.IsBool()) { preset = preset_it->value.GetBool(); } // build patch data PatchData patch_data { .enabled = false, .game_code = game_code, .datecode_min = 0, .datecode_max = 0, .name = name_it->value.GetString(), .description = description_it->value.GetString(), .caution = "", .name_in_lower_case = strtolower(name_it->value.GetString()), .type = PatchType::Unknown, .preset = preset, .patches_memory = std::vector(), .patches_union = std::vector(), .patch_number = NumberPatch(), .group_id = resolve_patch_group_id( patch, group_definitions, game_code, name_it->value.GetString()), .last_status = PatchStatus::Disabled, .hash = "", .unverified = false, .peIdentifier = "", .error_reason = "", .selected_union_name = "", }; // determine patch type auto type_str = type_it->value.GetString(); if (!_stricmp(type_str, "memory")) { patch_data.type = PatchType::Memory; } else if (!_stricmp(type_str, "signature")) { patch_data.type = PatchType::Signature; } // determine date code auto date_code_it = patch.FindMember("dateCode"); if (date_code_it != patch.MemberEnd() && date_code_it->value.IsInt()) { patch_data.datecode_min = date_code_it->value.GetInt(); patch_data.datecode_max = patch_data.datecode_min; } else { auto date_code_min_it = patch.FindMember("dateCodeMin"); if (date_code_min_it == patch.MemberEnd() || !date_code_min_it->value.IsInt()) { log_warning("patchmanager", "unable to parse datecode for {}", name_it->value.GetString()); continue; } auto date_code_max_it = patch.FindMember("dateCodeMax"); if (date_code_max_it == patch.MemberEnd() || !date_code_max_it->value.IsInt()) { log_warning("patchmanager", "unable to parse datecode for {}", name_it->value.GetString()); continue; } patch_data.datecode_min = date_code_min_it->value.GetInt(); patch_data.datecode_max = date_code_max_it->value.GetInt(); } // check for skip if (!avs::game::is_model(patch_data.game_code.c_str())) { continue; } if (!avs::game::is_ext(patch_data.datecode_min, patch_data.datecode_max)) { continue; } // generate hash patch_data.hash = patch_hash(patch_data); // check for existing bool existing = false; for (auto &added_patch : patches) { if (added_patch.hash == patch_data.hash) { existing = true; break; } } if (existing) { continue; } // hash check for enabled for (auto &enabled_entry : setting_patches_enabled) { if (patch_data.hash == enabled_entry) { patch_data.enabled = true; break; } } // check patch type switch (patch_data.type) { case PatchType::Memory: { // iterate memory patches auto patches_it = patch.FindMember("patches"); if (patches_it == patch.MemberEnd() || !patches_it->value.IsArray()) { log_warning("patchmanager", "unable to get patches for {}", name_it->value.GetString()); continue; } for (auto &memory_patch : patches_it->value.GetArray()) { // validate data auto data_disabled_it = memory_patch.FindMember("dataDisabled"); if (data_disabled_it == memory_patch.MemberEnd() || !data_disabled_it->value.IsString()) { log_warning("patchmanager", "unable to get data for {}", name_it->value.GetString()); continue; } auto data_enabled_it = memory_patch.FindMember("dataEnabled"); if (data_enabled_it == memory_patch.MemberEnd() || !data_enabled_it->value.IsString()) { log_warning("patchmanager", "unable to get data for {}", name_it->value.GetString()); continue; } // get hex strings auto data_disabled_hex = data_disabled_it->value.GetString(); auto data_enabled_hex = data_enabled_it->value.GetString(); auto data_disabled_hex_len = strlen(data_disabled_hex); auto data_enabled_hex_len = strlen(data_enabled_hex); if ((data_disabled_hex_len % 2) != 0 || (data_enabled_hex_len % 2) != 0) { log_warning("patchmanager", "patch hex data length has odd length for {}", name_it->value.GetString()); continue; } // convert to binary std::shared_ptr data_disabled(new uint8_t[data_disabled_hex_len / 2]); std::shared_ptr data_enabled(new uint8_t[data_enabled_hex_len / 2]); if (!hex2bin(data_disabled_hex, data_disabled.get()) || (!hex2bin(data_enabled_hex, data_enabled.get()))) { log_warning("patchmanager", "failed to parse patch data from hex for {}", name_it->value.GetString()); continue; } // get DLL name auto dll_name_it = memory_patch.FindMember("dllName"); if (dll_name_it == memory_patch.MemberEnd() || !dll_name_it->value.IsString()) { log_warning("patchmanager", "unable to get dllName for {}", name_it->value.GetString()); continue; } std::string dll_name = dll_name_it->value.GetString(); // IIDX omnimix dll name fix if (dll_name == "bm2dx.dll" && avs::game::is_model("LDJ") && avs::game::REV[0] == 'X') { dll_name = avs::game::DLL_NAME; } // BST 1/2 combined release dll name fix if (dll_name == "beatstream.dll" && (avs::game::DLL_NAME == "beatstream1.dll" || avs::game::DLL_NAME == "beatstream2.dll")) { dll_name = avs::game::DLL_NAME; } // build memory patch data MemoryPatch memory_patch_data { .dll_name = dll_name, .data_disabled = std::move(data_disabled), .data_disabled_len = data_disabled_hex_len / 2, .data_enabled = std::move(data_enabled), .data_enabled_len = data_enabled_hex_len / 2, .data_offset = 0, }; // get data offset auto data_offset_it = memory_patch.FindMember("dataOffset"); if (data_offset_it == memory_patch.MemberEnd()) { log_warning("patchmanager", "unable to get dataOffset for {}", name_it->value.GetString()); continue; } if (data_offset_it->value.IsUint64()) { memory_patch_data.data_offset = data_offset_it->value.GetUint64(); } else if (data_offset_it->value.IsString()) { std::stringstream ss; ss << data_offset_it->value.GetString(); ss >> memory_patch_data.data_offset; if (!ss.good() || !ss.eof()) { log_warning("patchmanager", "invalid dataOffset for {}", name_it->value.GetString()); continue; } } else { log_warning("patchmanager", "unable to get dataOffset for {}", name_it->value.GetString()); continue; } // move to list patch_data.patches_memory.emplace_back(memory_patch_data); } break; } case PatchType::Signature: { // validate data auto data_signature_it = patch.FindMember("signature"); if (data_signature_it == patch.MemberEnd() || !data_signature_it->value.IsString()) { log_warning("patchmanager", "unable to get data for {}", name_it->value.GetString()); continue; } auto data_replacement_it = patch.FindMember("replacement"); if (data_replacement_it == patch.MemberEnd() || !data_replacement_it->value.IsString()) { log_warning("patchmanager", "unable to get data for {}", name_it->value.GetString()); continue; } // get DLL name auto dll_name_it = patch.FindMember("dllName"); if (dll_name_it == patch.MemberEnd() || !dll_name_it->value.IsString()) { log_warning("patchmanager", "unable to get dllName for {}", name_it->value.GetString()); continue; } std::string dll_name = dll_name_it->value.GetString(); // IIDX omnimix dll name fix if (dll_name == "bm2dx.dll" && avs::game::is_model("LDJ") && avs::game::REV[0] == 'X') { dll_name = avs::game::DLL_NAME; } // BST 1/2 combined release dll name fix if (dll_name == "beatstream.dll" && (avs::game::DLL_NAME == "beatstream1.dll" || avs::game::DLL_NAME == "beatstream2.dll")) { dll_name = avs::game::DLL_NAME; } // get optional offset uint64_t offset = 0; auto offset_it = patch.FindMember("offset"); if (offset_it != patch.MemberEnd()) { bool invalid = false; if (offset_it->value.IsInt64()) { offset = offset_it->value.GetInt64(); } else if (offset_it->value.IsString()) { std::stringstream ss; ss << offset_it->value.GetString(); ss >> offset; invalid = !ss.good() || !ss.eof(); } else { invalid = true; } if (invalid) { log_warning("patchmanager", "invalid offset for {}", name_it->value.GetString()); } } // get optional usage int usage = 0; auto usage_it = patch.FindMember("usage"); if (usage_it != patch.MemberEnd()) { bool invalid = false; if (usage_it->value.IsInt64()) { usage = usage_it->value.GetInt64(); } else if (usage_it->value.IsString()) { std::stringstream ss; ss << usage_it->value.GetString(); ss >> usage; invalid = !ss.good() || !ss.eof(); } else { invalid = true; } if (invalid) { log_warning("patchmanager", "invalid usage for {}", name_it->value.GetString()); } } // build signature patch SignaturePatch signature_data = { .dll_name = dll_name, .signature = data_signature_it->value.GetString(), .replacement = data_replacement_it->value.GetString(), .offset = offset, .usage = usage, }; // convert to memory patch patch_data.patches_memory.emplace_back(signature_data.to_memory(&patch_data)); patch_data.type = PatchType::Memory; break; } case PatchType::Unknown: default: log_warning("patchmanager", "unknown patch type: {}", static_cast(patch_data.type)); break; } // auto apply register_patch_group(patch_data, group_definitions); if (apply_patches && setting_auto_apply && patch_data.enabled) { print_auto_apply_status(patch_data); apply_patch(patch_data, true); } // remember patch patches.emplace_back(patch_data); } } bool import_remote_patches_for_dll(const std::string& url, const std::string& dll_name) { log_info("patchmanager", "loading remote patches for {}...", dll_name); std::string identifier = get_game_identifier(MODULE_PATH / dll_name); std::string url_cpy = url; if (url_cpy.back() != '/') url_cpy += '/'; std::string json_path = fmt::format("{}{}.json", url_cpy, identifier); try { auto patches_json = getFromUrl(dll_name, json_path); if (!patches_json.empty()) { // see if this is valid JSON Document doc_temp; doc_temp.Parse(patches_json.c_str()); const auto error = doc_temp.GetParseError(); if (error) { log_warning( "patchmanager", "remotely fetched JSON file parse error: {}", rapidjson::GetParseError_En(error)); url_fetch_errors += fmt::format( "Invalid JSON received from remote URL.\n" "Your DLL version might not be supported.\n" "URL: {}\n" "JSON Parse Error: {}\n", json_path, rapidjson::GetParseError_En(error)); return false; } else { log_info("patchmanager", "remotely fetched JSON was successfully parsed"); } // create patches dir if (!fileutils::dir_exists(LOCAL_PATCHES_PATH)) { fileutils::dir_create(LOCAL_PATCHES_PATH); } // save to file std::filesystem::path save_path = LOCAL_PATCHES_PATH / (identifier + ".json"); fileutils::text_write(save_path, patches_json); log_info("patchmanager", "remotely fetched JSON saved to: {}", save_path); return true; } else { log_warning("patchmanager", "failed to fetch patches JSON for {}", dll_name); } } catch (const std::exception& e) { log_warning("patchmanager", "exception occurred while loading remote patches JSON for {}: {}", dll_name, e.what()); url_fetch_errors += fmt::format( "Exception while loading remote patches for {}: {}\n", dll_name, e.what()); } return false; } bool load_from_patches_json(bool apply_patches) { bool ret = false; // list valid PE identifiers from our local files auto modules = std::vector(); modules.push_back(get_game_identifier(MODULE_PATH / avs::game::DLL_NAME)); for (const std::string& dll : getExtraDlls(avs::game::DLL_NAME)) { modules.push_back(get_game_identifier(MODULE_PATH / dll)); } auto filter = [&modules](const PatchData& patch_data) { // match on peIdentifier if provided if (!patch_data.peIdentifier.empty()) { return std::ranges::find(modules, patch_data.peIdentifier) != modules.end(); } // game code is already checked by append_patches, so no need to check here // check the datecode / datecode range, if it exists if (patch_data.datecode_min != 0 || patch_data.datecode_max != 0) { return avs::game::is_ext(patch_data.datecode_min, patch_data.datecode_max); } // otherwise, don't load them in return false; }; // possible locations of patches.json // note: MODULE_PATH changes at launch, so it must be checked fresh here // (as opposed to this being checked once at launch) const std::filesystem::path LOCAL_PATCHES_JSON_PATHS[] = { "patches/patches.json", // new in spice2x "patches.json", // spicetools MODULE_PATH / "patches.json", // spicetools std::filesystem::path("..") / "patches.json" // spicetools }; const size_t patches_size_previous = patches.size(); for (const std::filesystem::path& patches_json_path: LOCAL_PATCHES_JSON_PATHS) { if (!fileutils::file_exists(patches_json_path)) { log_misc("patchmanager", "file does not exist, skipping: {}", patches_json_path); continue; } log_misc("patchmanager", "reading from patches.json: {}", patches_json_path); std::string content = fileutils::text_read(patches_json_path); append_patches(content, apply_patches, filter); const auto new_patches = patches.size() - patches_size_previous; log_info("patchmanager", "loaded {} patches from: {}", new_patches, patches_json_path); if (0 < new_patches) { ret = true; ACTIVE_JSON_FILE = displayPath(patches_json_path); break; } } return ret; } void reload_local_patches(bool apply_patches) { // announce reload if (apply_patches) { log_info("patchmanager", "reloading (local) and applying patches"); } else { log_info("patchmanager", "reloading (local) patches"); } // clear old patches patches.clear(); patch_groups.clear(); // drop the cached sorted view so the table rebuilds it (in default order) // on the next frame patches_sorted.clear(); if (cfg::CONFIGURATOR_STANDALONE) { clear_dll_maps(); } ACTIVE_JSON_FILE = ""; std::string firstDll = avs::game::DLL_NAME; std::string first_id = get_game_identifier(MODULE_PATH / firstDll, true); std::filesystem::path firstPath = fmt::format("patches/{}.json", first_id); auto extraDlls = getExtraDlls(firstDll); std::erase_if(extraDlls, [](const std::string& dll) { auto identifier = get_game_identifier(MODULE_PATH / dll, true); return identifier.empty() || !fileutils::file_exists(fmt::format("patches/{}.json", identifier)); }); if (fileutils::file_exists(firstPath) || !extraDlls.empty()) { if (fileutils::file_exists(firstPath)) { log_info("patchmanager", "loaded patches for {} from {}", firstDll, firstPath); std::string content = fileutils::text_read(firstPath); append_patches(content, apply_patches, nullptr, first_id); ACTIVE_JSON_FILE = displayPath(firstPath); } for (const std::string& dll : extraDlls) { auto extraId = get_game_identifier(MODULE_PATH / dll); auto extraPath = std::filesystem::path(fmt::format("patches/{}.json", extraId)); log_info("patchmanager", "loaded patches for {} from {}", dll, extraPath); std::string content = fileutils::text_read(extraPath); append_patches(content, apply_patches, nullptr, extraId); if (ACTIVE_JSON_FILE.empty()) { ACTIVE_JSON_FILE = displayPath(extraPath); } else { ACTIVE_JSON_FILE += ", " + displayPath(extraPath); } } } else { load_from_patches_json(apply_patches); } if (patches.empty()) { // load embedded patches from resources load_embedded_patches(apply_patches); } // show amount of patches log_info("patchmanager", "loaded total of {} patches", patches.size()); local_patches_initialized = true; } bool import_remote_patches_to_disk() { bool imported = false; // clear old patches patches.clear(); patch_groups.clear(); patches_sorted.clear(); url_fetch_errors.clear(); // load patches for main dll imported = import_remote_patches_for_dll(patch_url, avs::game::DLL_NAME); // check for additional patches based on module name for (const std::string& dll : getExtraDlls(avs::game::DLL_NAME)) { imported |= import_remote_patches_for_dll(patch_url, dll); } return imported; } void append_patches( std::string &patches_json, bool apply_patches, std::function filter, std::string pe_identifier_for_patch) { // parse document Document doc; doc.Parse(patches_json.c_str()); // check parse error const auto error = doc.GetParseError(); const auto error_offset = doc.GetErrorOffset(); if (error) { log_warning( "patchmanager", "patches file parse error at offset {}: {} ({})", error_offset, static_cast(error), rapidjson::GetParseError_En(error)); } const auto group_definitions = parse_patch_group_definitions(doc); // iterate patches for (auto &patch : doc.GetArray()) { if (is_patch_group_definition(patch)) { continue; } // verfiy patch data auto name_it = patch.FindMember("name"); if (name_it == patch.MemberEnd() || !name_it->value.IsString()) { if (patch == doc.GetArray()[0]) { // first one is special - it may be header info, print it out to console rapidjson::StringBuffer buffer; PrettyWriter writer(buffer); patch.Accept(writer); log_info("patchmanager", "patches file info: \n{}", buffer.GetString()); } else { log_warning("patchmanager", "failed to parse patch name"); } continue; } auto game_code_it = patch.FindMember("gameCode"); if (game_code_it == patch.MemberEnd() || !game_code_it->value.IsString()) { log_warning("patchmanager", "failed to parse game code for {}", name_it->value.GetString()); continue; } const std::string game_code( game_code_it->value.GetString(), game_code_it->value.GetStringLength()); auto description_it = patch.FindMember("description"); if (description_it == patch.MemberEnd() || !description_it->value.IsString()) { log_warning("patchmanager", "failed to parse description for {}", name_it->value.GetString()); continue; } auto caution_it = patch.FindMember("caution"); const char* caution = ""; if (caution_it != patch.MemberEnd() && caution_it->value.IsString()) { caution = caution_it->value.GetString(); } auto type_it = patch.FindMember("type"); if (type_it == patch.MemberEnd() || !type_it->value.IsString()) { log_warning("patchmanager", "failed to parse type for {}", name_it->value.GetString()); continue; } auto pe_identifier_it = patch.FindMember("peIdentifier"); const char* pe_identifier = ""; if (pe_identifier_it != patch.MemberEnd() && pe_identifier_it->value.IsString()) { pe_identifier = pe_identifier_it->value.GetString(); } auto preset_it = patch.FindMember("preset"); bool preset = false; if (preset_it != patch.MemberEnd() && preset_it->value.IsBool()) { preset = preset_it->value.GetBool(); } // build patch data PatchData patch_data { .enabled = false, .game_code = game_code, .datecode_min = 0, .datecode_max = 0, .name = name_it->value.GetString(), .description = description_it->value.GetString(), .caution = std::string(caution), .name_in_lower_case = strtolower(name_it->value.GetString()), .type = PatchType::Unknown, .preset = preset, .patches_memory = std::vector(), .patches_union = std::vector(), .patch_number = NumberPatch(), .group_id = resolve_patch_group_id( patch, group_definitions, game_code, name_it->value.GetString()), .last_status = PatchStatus::Disabled, .hash = "", .unverified = false, .peIdentifier = std::string(pe_identifier), .error_reason = "", .selected_union_name = "", }; // determine date code const auto date_code_it = patch.FindMember("dateCode"); if (date_code_it != patch.MemberEnd() && date_code_it->value.IsInt()) { patch_data.datecode_min = date_code_it->value.GetInt(); patch_data.datecode_max = patch_data.datecode_min; } else { const auto date_code_min_it = patch.FindMember("dateCodeMin"); const auto date_code_max_it = patch.FindMember("dateCodeMax"); if (date_code_min_it != patch.MemberEnd() && date_code_min_it->value.IsInt() && date_code_max_it != patch.MemberEnd() && date_code_max_it->value.IsInt()) { patch_data.datecode_min = date_code_min_it->value.GetInt(); patch_data.datecode_max = date_code_max_it->value.GetInt(); } } // override pe identifier if it wasn't present in JSON (possible for remote patches) if (patch_data.peIdentifier.empty()) { patch_data.peIdentifier = pe_identifier_for_patch; } // if the caller provided a filter, check if this patch should be ignored. if (filter && !filter(patch_data)) { continue; } // determine patch type const auto type_str = type_it->value.GetString(); if (!_stricmp(type_str, "memory")) { patch_data.type = PatchType::Memory; } else if (!_stricmp(type_str, "signature")) { patch_data.type = PatchType::Signature; } else if (!stricmp(type_str, "union")) { patch_data.type = PatchType::Union; } else if (!stricmp(type_str, "number")) { patch_data.type = PatchType::Integer; } // check for skip if (!avs::game::is_model(patch_data.game_code.c_str())) { continue; } // generate hash patch_data.hash = patch_hash(patch_data); // check for existing bool existing = false; for (auto &added_patch : patches) { if (added_patch.hash == patch_data.hash) { existing = true; break; } } if (existing) { continue; } // hash check for enabled if (patch_data.type == PatchType::Union) { if (setting_union_patches_enabled.contains(patch_data.hash)) { patch_data.enabled = true; } } else if (patch_data.type == PatchType::Integer) { if (setting_int_patches_enabled.contains(patch_data.hash)) { patch_data.enabled = true; } } else { for (auto &enabled_entry : setting_patches_enabled) { if (patch_data.hash == enabled_entry) { patch_data.enabled = true; break; } } } // check patch type switch (patch_data.type) { case PatchType::Memory: { // iterate memory patches auto patches_it = patch.FindMember("patches"); if (patches_it == patch.MemberEnd() || !patches_it->value.IsArray()) { log_warning("patchmanager", "unable to get patches for {}", name_it->value.GetString()); continue; } for (auto& memory_patch : patches_it->value.GetArray()) { // validate data auto data_disabled_it = memory_patch.FindMember("dataDisabled"); if (data_disabled_it == memory_patch.MemberEnd() || !data_disabled_it->value.IsString()) { log_warning("patchmanager", "unable to get data for {}", name_it->value.GetString()); continue; } auto data_enabled_it = memory_patch.FindMember("dataEnabled"); if (data_enabled_it == memory_patch.MemberEnd() || !data_enabled_it->value.IsString()) { log_warning("patchmanager", "unable to get data for {}", name_it->value.GetString()); continue; } // get hex strings auto data_disabled_hex = data_disabled_it->value.GetString(); auto data_enabled_hex = data_enabled_it->value.GetString(); auto data_disabled_hex_len = strlen(data_disabled_hex); auto data_enabled_hex_len = strlen(data_enabled_hex); if ((data_disabled_hex_len % 2) != 0 || (data_enabled_hex_len % 2) != 0) { log_warning("patchmanager", "patch hex data length has odd length for {}", name_it->value.GetString()); continue; } // convert to binary std::shared_ptr data_disabled(new uint8_t[data_disabled_hex_len / 2]); std::shared_ptr data_enabled(new uint8_t[data_enabled_hex_len / 2]); if (!hex2bin(data_disabled_hex, data_disabled.get()) || (!hex2bin(data_enabled_hex, data_enabled.get()))) { log_warning("patchmanager", "failed to parse patch data from hex for {}", name_it->value.GetString()); continue; } // get DLL name auto dll_name_it = memory_patch.FindMember("dllName"); if (dll_name_it == memory_patch.MemberEnd() || !dll_name_it->value.IsString()) { log_warning("patchmanager", "unable to get dllName for {}", name_it->value.GetString()); continue; } const std::string dll_name = fix_up_dll_name(dll_name_it->value.GetString()); // build memory patch data MemoryPatch memory_patch_data { .dll_name = dll_name, .data_disabled = std::move(data_disabled), .data_disabled_len = data_disabled_hex_len / 2, .data_enabled = std::move(data_enabled), .data_enabled_len = data_enabled_hex_len / 2, .data_offset = 0, }; // get data offset memory_patch_data.data_offset = parse_json_data_offset(patch_data.name, memory_patch); if (memory_patch_data.data_offset == 0) { continue; } // move to list patch_data.patches_memory.emplace_back(memory_patch_data); } break; } case PatchType::Signature: { // validate data auto data_signature_it = patch.FindMember("signature"); if (data_signature_it == patch.MemberEnd() || !data_signature_it->value.IsString()) { log_warning("patchmanager", "unable to get data for {}", name_it->value.GetString()); continue; } auto data_replacement_it = patch.FindMember("replacement"); if (data_replacement_it == patch.MemberEnd() || !data_replacement_it->value.IsString()) { log_warning("patchmanager", "unable to get data for {}", name_it->value.GetString()); continue; } // get DLL name auto dll_name_it = patch.FindMember("dllName"); if (dll_name_it == patch.MemberEnd() || !dll_name_it->value.IsString()) { log_warning("patchmanager", "unable to get dllName for {}", name_it->value.GetString()); continue; } const std::string dll_name = fix_up_dll_name(dll_name_it->value.GetString()); // get optional offset uint64_t offset = parse_json_data_offset(patch_data.name, patch); // get optional usage int usage = 0; auto usage_it = patch.FindMember("usage"); if (usage_it != patch.MemberEnd()) { bool invalid = false; if (usage_it->value.IsInt64()) { usage = usage_it->value.GetInt64(); } else if (usage_it->value.IsString()) { std::stringstream ss; ss << usage_it->value.GetString(); ss >> usage; invalid = !ss.good() || !ss.eof(); } else { invalid = true; } if (invalid) { log_warning("patchmanager", "invalid usage for {}", name_it->value.GetString()); } } // build signature patch SignaturePatch signature_data = { .dll_name = dll_name, .signature = data_signature_it->value.GetString(), .replacement = data_replacement_it->value.GetString(), .offset = offset, .usage = usage, }; // convert to memory patch patch_data.patches_memory.emplace_back(signature_data.to_memory(&patch_data)); patch_data.type = PatchType::Memory; break; } case PatchType::Union: { // iterate union patches auto patches_it = patch.FindMember("patches"); if (patches_it == patch.MemberEnd() || !patches_it->value.IsArray()) { log_warning("patchmanager", "unable to get patches for {}", name_it->value.GetString()); continue; } std::string union_dll_name_for_patch(""); uint64_t union_offset_for_patch = 0; uint64_t union_hex_len_for_patch = 0; for (auto& union_patch : patches_it->value.GetArray()) { // validate data auto union_name_it = union_patch.FindMember("name"); if (union_name_it == union_patch.MemberEnd() || !union_name_it->value.IsString()) { log_warning("patchmanager", "unable to get name for {}", name_it->value.GetString()); continue; } auto union_patch_it = union_patch.FindMember("patch"); if (union_patch_it == union_patch.MemberEnd() || !union_patch_it->value.IsObject()) { log_warning("patchmanager", "unable to get patch for {}", name_it->value.GetString()); continue; } // get patch data auto union_dll_name_it = union_patch_it->value.FindMember("dllName"); if (union_dll_name_it == union_patch_it->value.MemberEnd() || !union_dll_name_it->value.IsString()) { log_warning("patchmanager", "unable to get dllName for {}", name_it->value.GetString()); continue; } auto union_data_it = union_patch_it->value.FindMember("data"); if (union_data_it == union_patch_it->value.MemberEnd() || !union_data_it->value.IsString()) { log_warning("patchmanager", "unable to get data for {}", name_it->value.GetString()); continue; } // get hex string auto union_data_hex = union_data_it->value.GetString(); auto union_data_hex_len = strlen(union_data_hex); if ((union_data_hex_len % 2) != 0) { log_warning("patchmanager", "patch hex data length has odd length for {}", name_it->value.GetString()); continue; } // convert to binary std::shared_ptr union_data(new uint8_t[union_data_hex_len / 2]); if (!hex2bin(union_data_hex, union_data.get())) { log_warning("patchmanager", "failed to parse patch data from hex for {}", name_it->value.GetString()); continue; } // get DLL name const std::string union_dll_name = fix_up_dll_name(union_dll_name_it->value.GetString()); if (union_dll_name_for_patch.empty()) { union_dll_name_for_patch = union_dll_name; } else if (union_dll_name != union_dll_name_for_patch) { log_warning( "patchmanager", "inconsistent DLL name for union patch '{}'::'{}', ignoring", name_it->value.GetString(), union_name_it->value.GetString()); continue; } // get offset uint64_t union_offset = parse_json_data_offset(patch_data.name, union_patch_it->value); if (union_offset == 0) { continue; } // validate that offset and size are the same for all patches for this union if (union_offset_for_patch == 0) { union_offset_for_patch = union_offset; } else if (union_offset_for_patch != union_offset) { log_warning( "patchmanager", "inconsistent offset detected for union patch '{}'::'{}', ignoring", name_it->value.GetString(), union_name_it->value.GetString()); continue; } if (union_hex_len_for_patch == 0) { union_hex_len_for_patch = union_data_hex_len; } else if (union_hex_len_for_patch != union_data_hex_len) { log_warning( "patchmanager", "inconsistent length detected for union patch '{}'::'{}', ignoring", name_it->value.GetString(), union_name_it->value.GetString()); continue; } // build union patch UnionPatch union_patch_data{ .name = union_name_it->value.GetString(), .dll_name = union_dll_name, .data = std::move(union_data), .data_len = union_data_hex_len / 2, .offset = union_offset, }; // move to list patch_data.patches_union.emplace_back(union_patch_data); } if (setting_union_patches_enabled.contains(patch_data.hash)) { patch_data.selected_union_name = setting_union_patches_enabled[patch_data.hash]; } break; } case PatchType::Integer: { auto& numpatch = patch_data.patch_number; auto num_patch_it = patch.FindMember("patch"); if (num_patch_it == patch.MemberEnd() || !num_patch_it->value.IsObject()) { log_warning("patchmanager", "unable to get patch for {}", name_it->value.GetString()); continue; } // validate data auto min_it = num_patch_it->value.FindMember("min"); if (min_it == num_patch_it->value.MemberEnd() || !min_it->value.IsNumber()) { log_warning( "patchmanager", "unable to get data for min - {}", min_it->value.GetString()); continue; } auto max_it = num_patch_it->value.FindMember("max"); if (max_it == num_patch_it->value.MemberEnd() || !max_it->value.IsNumber()) { log_warning( "patchmanager", "unable to get data for max - {}", max_it->value.GetString()); continue; } auto size_it = num_patch_it->value.FindMember("size"); if (size_it == num_patch_it->value.MemberEnd() || !size_it->value.IsNumber()) { log_warning( "patchmanager", "unable to get data for size - {}", size_it->value.GetString()); continue; } // get values auto min = min_it->value.GetInt(); auto max = max_it->value.GetInt(); auto size = size_it->value.GetUint(); if (min >= max) { log_warning( "patchmanager", "invalid min/max range provided for patch: {}", name_it->value.GetString()); continue; } if (size != 1 && size != 2 && size != 4 && size != 8) { log_warning( "patchmanager", "invalid size provided for patch: {}", name_it->value.GetString()); continue; } // get DLL name auto dll_name_it = num_patch_it->value.FindMember("dllName"); if (dll_name_it == num_patch_it->value.MemberEnd() || !dll_name_it->value.IsString()) { log_warning( "patchmanager", "unable to get dllName for {}", name_it->value.GetString()); continue; } // build number patch data numpatch.dll_name = fix_up_dll_name(dll_name_it->value.GetString()); numpatch.min = min; numpatch.max = max; numpatch.size_in_bytes = size; // get data offset numpatch.data_offset = parse_json_data_offset(patch_data.name, num_patch_it->value); if (numpatch.data_offset == 0) { continue; } // load value from previously saved patch setting if (setting_int_patches_enabled.contains(patch_data.hash)) { numpatch.value = CLAMP( setting_int_patches_enabled[patch_data.hash], numpatch.min, numpatch.max); } break; } case PatchType::Unknown: default: log_warning("patchmanager", "unknown patch type: {}", static_cast(patch_data.type)); break; } // auto apply register_patch_group(patch_data, group_definitions); if (apply_patches && setting_auto_apply && patch_data.enabled) { print_auto_apply_status(patch_data); apply_patch(patch_data, true); } // remember patch patches.emplace_back(patch_data); } } uint64_t parse_json_data_offset( const std::string &patch_name, const rapidjson::Value &value) { // parse "offset" / "dataOffset" auto data_offset_it = value.FindMember("offset"); if (data_offset_it == value.MemberEnd()) { data_offset_it = value.FindMember("dataOffset"); if (data_offset_it == value.MemberEnd()) { log_warning("patchmanager", "unable to get offset / dataOffset for {}", patch_name); return 0; } } if (data_offset_it->value.IsUint64()) { // parse as unsigned integer return data_offset_it->value.GetUint64(); } else if (data_offset_it->value.IsString()) { // parse as string and convert to integer uint64_t offset; std::stringstream ss; ss << data_offset_it->value.GetString(); ss >> offset; if (!ss.good() || !ss.eof()) { log_warning("patchmanager", "invalid offset for {}", patch_name); return 0; } return offset; } else { log_warning("patchmanager", "unable to get offset for {}", patch_name); return 0; } } }