mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-01 14:20:42 -07:00
be4bb00390
## 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?*
1343 lines
58 KiB
C++
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;
|
|
}
|
|
}
|
|
|
|
}
|