Files
spice2x.github.io/src/spice2x/patcher/loader.cpp
T
bicarus be4bb00390 patcher: patch groups (#812)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #245

## Description of change

Introduce patch groups. JSON schema now allows for a `"type": "group"`:

```json
    {
        "type": "group",
        "gameCode": "LDJ",
        "id": "timer-freeze",
        "name": "Timer Freeze Patches",
        "description": "Freezes various timers in the game."
    },
    {
        "name": "Standard/Menu Timer Freeze",
        "description": "Freezes all non-premium area timers.",
        "caution": "",
        "gameCode": "LDJ",
        "type": "memory",
        "group": "timer-freeze",
        "patches": [
            {
                "offset": 9962743,
                "dllName": "bm2dx.dll",
                "dataDisabled": "0F84",
                "dataEnabled": "90E9"
            }
        ]
    },
    {
        "name": "Premium Free Timer Freeze",
        "description": "Freezes all premium area timers.",
        "caution": "",
        "gameCode": "LDJ",
        "type": "memory",
        "group": "timer-freeze",
        "patches": [
            {
                "offset": 9111965,
                "dllName": "bm2dx.dll",
                "dataDisabled": "7E",
                "dataEnabled": "EB"
            }
        ]
    },
```

In the UI, these will show up as tree nodes. The parent is not actually
treated as a patch; e.g., its state is not saved to the patch manager
config file; only the child patches states are managed, the parent's
state only bubble up only in the UI.

In downlevel versions of spice, group parents will show up as a patch of
invalid type. Child patches will still show up as individual patches.

## Testing
*how was the code tested?*
2026-07-19 02:10:13 -07:00

1343 lines
58 KiB
C++

#include "internal.h"
#include <algorithm>
#include <chrono>
#include <cstring>
#include <memory>
#include <sstream>
#include <windows.h>
#include <winhttp.h>
#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<std::string> 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<void, decltype(&WinHttpCloseHandle)>(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<void, decltype(&WinHttpCloseHandle)>(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<void, decltype(&WinHttpCloseHandle)>(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<char> 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<uint32_t>(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<MemoryPatch>(),
.patches_union = std::vector<UnionPatch>(),
.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<uint8_t[]> data_disabled(new uint8_t[data_disabled_hex_len / 2]);
std::shared_ptr<uint8_t[]> 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<uint32_t>(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<std::string>();
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<bool(const PatchData&)> 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<uint32_t>(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<StringBuffer> 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<MemoryPatch>(),
.patches_union = std::vector<UnionPatch>(),
.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<uint8_t[]> data_disabled(new uint8_t[data_disabled_hex_len / 2]);
std::shared_ptr<uint8_t[]> 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<uint8_t[]> 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<uint32_t>(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;
}
}
}