3000 lines
127 KiB
C++
3000 lines
127 KiB
C++
#include "patch_manager.h"
|
|
|
|
#include <thread>
|
|
#include <fstream>
|
|
#include <shellapi.h>
|
|
#include <winhttp.h>
|
|
#include <psapi.h>
|
|
#include <format>
|
|
#include "external/rapidjson/document.h"
|
|
#include "external/rapidjson/prettywriter.h"
|
|
#include "external/rapidjson/stringbuffer.h"
|
|
#include "external/rapidjson/error/en.h"
|
|
#include "external/hash-library/sha256.h"
|
|
#include "external/robin_hood.h"
|
|
#include "cfg/configurator.h"
|
|
#include "util/memutils.h"
|
|
#include "games/io.h"
|
|
#include "build/resource.h"
|
|
#include "util/sigscan.h"
|
|
#include "util/resutils.h"
|
|
#include "util/fileutils.h"
|
|
#include "util/libutils.h"
|
|
#include "util/logging.h"
|
|
#include "util/utils.h"
|
|
#include "util/netutils.h"
|
|
#include "overlay/imgui/extensions.h"
|
|
#include "avs/game.h"
|
|
#include "misc/clipboard.h"
|
|
|
|
// std::min
|
|
#ifdef min
|
|
#undef min
|
|
#endif
|
|
|
|
// std::max
|
|
#ifdef max
|
|
#undef max
|
|
#endif
|
|
|
|
using namespace rapidjson;
|
|
|
|
|
|
namespace overlay::windows {
|
|
|
|
robin_hood::unordered_map<std::string, std::unique_ptr<std::vector<uint8_t>>> DLL_MAP;
|
|
robin_hood::unordered_map<std::string, std::unique_ptr<std::vector<uint8_t>>> DLL_MAP_ORG;
|
|
|
|
// configuration
|
|
std::filesystem::path PatchManager::config_path;
|
|
bool PatchManager::config_dirty = false;
|
|
bool PatchManager::setting_auto_apply = false;
|
|
std::vector<std::string> PatchManager::setting_auto_apply_list;
|
|
std::vector<std::string> PatchManager::setting_patches_enabled;
|
|
std::map<std::string, std::string> PatchManager::setting_union_patches_enabled;
|
|
std::map<std::string, int64_t> PatchManager::setting_int_patches_enabled;
|
|
static std::string url_fetch_errors;
|
|
|
|
std::string PatchManager::patch_url("");
|
|
std::string PatchManager::patch_name_filter("");
|
|
|
|
std::filesystem::path PatchManager::LOCAL_PATCHES_PATH("patches");
|
|
std::string PatchManager::ACTIVE_JSON_FILE("");
|
|
|
|
std::map<std::string, std::vector<std::string>> EXTRA_DLLS = {
|
|
{"jubeat.dll", {"music_db.dll", "coin.dll"}},
|
|
{"arkmdxp3.dll", {"gamemdx.dll"}},
|
|
{"arkmdxp4.dll", {"gamemdx.dll"}},
|
|
{"arkmdxbio2.dll", {"gamemdx.dll"}},
|
|
{"arkndd.dll", {"gamendd.dll"}},
|
|
{"arkkep.dll", {"game.dll"}},
|
|
{"arkjc9.dll", {"gamejc9.dll"}},
|
|
{"arkkdm.dll", {"gamekdm.dll"}},
|
|
{"arkmmd.dll", {"gamemmd.dll"}},
|
|
{"arkklp.dll", {"lpac.dll"}},
|
|
{"arknck.dll", {"weac.dll"}},
|
|
{"gdxg.dll", {"game.dll"}}
|
|
};
|
|
|
|
static size_t url_recent_idx = -1;
|
|
std::vector<std::string> url_recents = {};
|
|
|
|
std::vector<std::string> getExtraDlls(const std::string& firstDll) {
|
|
if (!EXTRA_DLLS.contains(firstDll)) {
|
|
return {};
|
|
}
|
|
return EXTRA_DLLS[firstDll];
|
|
}
|
|
|
|
// utility
|
|
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)) {
|
|
log_warning("patchmanager", "failed to crack URL: {}", GetLastError());
|
|
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) {
|
|
log_warning("patchmanager", "failed to open session: {}", GetLastError());
|
|
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) {
|
|
log_warning("patchmanager", "failed to open connect: {}", GetLastError());
|
|
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) {
|
|
log_warning("patchmanager", "failed to open request: {}", GetLastError());
|
|
return result;
|
|
}
|
|
|
|
if (!WinHttpSendRequest(request, nullptr, 0, nullptr, 0, 0, 0)) {
|
|
log_warning("patchmanager", "failed to send request: {}", GetLastError());
|
|
return result;
|
|
}
|
|
|
|
if (!WinHttpReceiveResponse(request, nullptr)) {
|
|
log_warning("patchmanager", "failed to receive response: {}", GetLastError());
|
|
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)) {
|
|
log_warning("patchmanager", "failed to query status code: {}", GetLastError());
|
|
return result;
|
|
}
|
|
|
|
if (statusCode != 200) {
|
|
log_warning("patchmanager", "failed to fetch URL: got unexpected status code {}", statusCode);
|
|
url_fetch_errors +=
|
|
fmt::format(
|
|
"\n{}: HTTP Status: {} ({})\n",
|
|
dll_name,
|
|
statusCode,
|
|
netutils::http_status_reason_phrase(statusCode));
|
|
if (statusCode == 404) {
|
|
url_fetch_errors += "(No patches found 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;
|
|
}
|
|
|
|
// patches
|
|
std::vector<PatchData> PatchManager::patches;
|
|
bool PatchManager::local_patches_initialized = false;
|
|
|
|
PatchManager::PatchManager(SpiceOverlay *overlay, bool apply_patches) : Window(overlay) {
|
|
this->title = "Patch Manager";
|
|
this->flags |= ImGuiWindowFlags_AlwaysAutoResize;
|
|
this->toggle_button = games::OverlayButtons::TogglePatchManager;
|
|
this->init_pos = ImVec2(10, 10);
|
|
this->config_path = std::filesystem::path(_wgetenv(L"APPDATA")) / L"spicetools_patch_manager.json";
|
|
if (!local_patches_initialized) {
|
|
patch_url.clear();
|
|
if (cfg::CONFIGURATOR_STANDALONE) {
|
|
apply_patches = true;
|
|
}
|
|
if (apply_patches) {
|
|
if (fileutils::file_exists(this->config_path)) {
|
|
this->config_load();
|
|
}
|
|
this->reload_local_patches(apply_patches);
|
|
}
|
|
}
|
|
}
|
|
|
|
PatchManager::~PatchManager() = default;
|
|
|
|
void PatchManager::build_content() {
|
|
|
|
// check if initialized
|
|
if (!local_patches_initialized) {
|
|
if (fileutils::file_exists(config_path)) {
|
|
this->config_load();
|
|
}
|
|
this->reload_local_patches();
|
|
}
|
|
|
|
// game code info
|
|
std::string identifiers;
|
|
identifiers += avs::game::get_identifier() + "\n\n";
|
|
identifiers += avs::game::DLL_NAME + " / " + get_game_identifier(MODULE_PATH / avs::game::DLL_NAME) + "\n";
|
|
|
|
for (const auto& dll : getExtraDlls(avs::game::DLL_NAME)) {
|
|
const auto dll_path = MODULE_PATH / dll;
|
|
if (fileutils::file_exists(dll_path)) {
|
|
identifiers += dll + " / " + get_game_identifier(dll_path) + "\n";
|
|
}
|
|
}
|
|
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::HelpMarker(identifiers.c_str());
|
|
ImGui::SameLine();
|
|
ImGui::Text("%s", avs::game::get_identifier().c_str());
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Copy")) {
|
|
clipboard::copy_text(identifiers.c_str());
|
|
}
|
|
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::HelpMarker(
|
|
"Path being used to look for DLLs, used for reading PE header values.\n"
|
|
"Wrong path? Run spicecfg from the correct directory, or fix your modules parameter before launching spicecfg.\n"
|
|
"Make sure you're not using a different one when launching the game.");
|
|
ImGui::SameLine();
|
|
ImGui::Text("Modules Path: %s", MODULE_PATH.string().c_str());
|
|
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::DummyMarker();
|
|
ImGui::SameLine();
|
|
if (ACTIVE_JSON_FILE.empty()) {
|
|
ImGui::Text("Patches JSON: built-in");
|
|
} else {
|
|
ImGui::Text("Patches JSON: %s", ACTIVE_JSON_FILE.c_str());
|
|
}
|
|
|
|
// auto apply checkbox
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::HelpMarker(
|
|
"This option is saved per game, using the date code.\n"
|
|
"When checked, all set patches will be applied on game boot."
|
|
);
|
|
ImGui::SameLine();
|
|
if (ImGui::Checkbox("Auto apply patches on game start", &setting_auto_apply)) {
|
|
config_dirty = true;
|
|
}
|
|
|
|
// check for dirty state
|
|
if (config_dirty) {
|
|
if (cfg::CONFIGURATOR_STANDALONE) {
|
|
// auto save for configurator version
|
|
this->config_save();
|
|
|
|
} else {
|
|
// manual save for live version
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::HelpMarker("Save current patch state to the configuration file.");
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Save")) {
|
|
this->config_save();
|
|
}
|
|
}
|
|
}
|
|
|
|
bool disable_all_patches = false;
|
|
if (cfg::CONFIGURATOR_STANDALONE) {
|
|
// import from url
|
|
// only allow import in the configurator (and not in-game)
|
|
// e.g., for public game set ups with keyboard access
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::DummyMarker();
|
|
ImGui::SameLine();
|
|
ImGui::BeginDisabled(avs::game::DLL_NAME.empty());
|
|
if (ImGui::Button("Import from URL##Button")) {
|
|
ImGui::OpenPopup("Import from URL");
|
|
}
|
|
ImGui::EndDisabled();
|
|
if (avs::game::DLL_NAME.empty()) {
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::DummyMarker();
|
|
ImGui::SameLine();
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.f, 1.f, 0.f, 1.f));
|
|
ImGui::TextUnformatted("WARNING: Game DLL is not found, fix modules parameter! Importing is disabled.");
|
|
ImGui::PopStyleColor();
|
|
}
|
|
|
|
// overwrite DLL
|
|
ImGui::SameLine();
|
|
if (!patches.empty()) {
|
|
if (ImGui::Button("Overwrite game files##Button")) {
|
|
ImGui::OpenPopup("Overwrite game files?");
|
|
}
|
|
}
|
|
if (ImGui::BeginPopupModal("Overwrite game files?", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.f, 1.f, 0.f, 1.f)); // yellow
|
|
ImGui::PushTextWrapPos(ImGui::GetIO().DisplaySize.x * 0.5);
|
|
ImGui::Text(
|
|
"Are you sure you want to permanently apply patches to your game files?");
|
|
ImGui::PopStyleColor();
|
|
ImGui::Text(
|
|
"File backups are made, but it's recommended that you keep your own copies.");
|
|
ImGui::PopTextWrapPos();
|
|
ImGui::Separator();
|
|
if (ImGui::Button("Yes, overwrite")) {
|
|
hard_apply_patches();
|
|
reload_local_patches();
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Cancel")) {
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
// disable all
|
|
ImGui::SameLine();
|
|
if (!patches.empty()) {
|
|
disable_all_patches = ImGui::Button("Disable all");
|
|
if (disable_all_patches) {
|
|
// reset auto apply now, and disable every patch down below
|
|
config_dirty = true;
|
|
setting_auto_apply = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool url_entered = false;
|
|
bool is_valid_url = false;
|
|
bool patches_imported = false;
|
|
|
|
// import from URL popup dialog
|
|
if (ImGui::BeginPopupModal("Import from URL", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
if (ImGui::TreeNodeEx("Warning - use at your own risk!", ImGuiTreeNodeFlags_DefaultOpen)) {
|
|
ImGui::PushTextWrapPos(ImGui::GetIO().DisplaySize.x * 0.6);
|
|
ImGui::Text(
|
|
"Only import patches from a trusted source. "
|
|
"These services are provided by third parties and may contain faulty or malicious code. "
|
|
"Game datecode and PE header information will be sent in the request.");
|
|
ImGui::PopTextWrapPos();
|
|
ImGui::TreePop();
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
|
|
if (ImGui::TreeNodeEx("Enter URL", ImGuiTreeNodeFlags_DefaultOpen)) {
|
|
ImGui::SetNextItemWidth(360.f);
|
|
ImGui::InputTextWithHint(
|
|
"##url_textinput",
|
|
"http://www.example.com",
|
|
&patch_url,
|
|
ImGuiInputTextFlags_CharsNoBlank | ImGuiInputTextFlags_AutoSelectAll);
|
|
|
|
if (ImGui::Button("Paste")) {
|
|
auto clipboard_url = clipboard::paste_text();
|
|
if (!clipboard_url.empty()) {
|
|
strreplace(clipboard_url, "\r\n", "");
|
|
strreplace(clipboard_url, " ", "");
|
|
patch_url = clipboard_url;
|
|
}
|
|
}
|
|
ImGui::SameLine();
|
|
ImGui::BeginDisabled(patch_url.empty());
|
|
if (ImGui::Button("Clear")) {
|
|
patch_url.clear();
|
|
url_recent_idx = -1;
|
|
}
|
|
ImGui::EndDisabled();
|
|
|
|
ImGui::TreePop();
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
|
|
// history
|
|
if (ImGui::TreeNodeEx("History", ImGuiTreeNodeFlags_DefaultOpen)) {
|
|
if (ImGui::BeginListBox(
|
|
"##url_recents",
|
|
ImVec2(360.f, 3 * ImGui::GetTextLineHeightWithSpacing()))) {
|
|
|
|
for (size_t i = 0; i < url_recents.size(); i++) {
|
|
const bool is_selected = (url_recent_idx == i);
|
|
if (ImGui::Selectable(url_recents[i].c_str(), is_selected)) {
|
|
url_recent_idx = i;
|
|
patch_url = url_recents[i];
|
|
}
|
|
}
|
|
ImGui::EndListBox();
|
|
}
|
|
ImGui::BeginDisabled(url_recent_idx == (size_t)(-1));
|
|
if (ImGui::Button("Remove selected")) {
|
|
url_recents.erase(url_recents.begin() + url_recent_idx);
|
|
url_recent_idx = -1;
|
|
this->config_save();
|
|
}
|
|
ImGui::EndDisabled();
|
|
ImGui::SameLine();
|
|
ImGui::BeginDisabled(url_recents.empty());
|
|
if (ImGui::Button("Clear all")) {
|
|
url_recents.clear();
|
|
url_recent_idx = -1;
|
|
this->config_save();
|
|
}
|
|
ImGui::EndDisabled();
|
|
ImGui::TreePop();
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
ImGui::BeginDisabled(patch_url.empty());
|
|
if (ImGui::Button("Import")) {
|
|
url_entered = true;
|
|
if (patch_url.find("http://") == 0 || patch_url.find("https://") == 0) {
|
|
is_valid_url = true;
|
|
}
|
|
url_recent_idx = -1;
|
|
}
|
|
ImGui::EndDisabled();
|
|
|
|
if (is_valid_url) {
|
|
patches_imported = import_remote_patches_to_disk();
|
|
if (patches_imported) {
|
|
if (std::find(url_recents.begin(), url_recents.end(), patch_url) == url_recents.end()) {
|
|
url_recents.emplace_back(patch_url);
|
|
}
|
|
this->config_save();
|
|
}
|
|
reload_local_patches();
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Cancel")) {
|
|
patch_url.clear();
|
|
url_recent_idx = -1;
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
// invalid URL popup dialog
|
|
if (url_entered && !is_valid_url) {
|
|
ImGui::OpenPopup("URL error");
|
|
}
|
|
if (ImGui::BeginPopupModal("URL error", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
ImGui::Text("Make sure URL starts with http:// or https://");
|
|
ImGui::Separator();
|
|
if (ImGui::Button("OK")) {
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
// URL import failure dialog
|
|
if (url_entered && is_valid_url && !patches_imported) {
|
|
ImGui::OpenPopup("Import failed##URLImport");
|
|
}
|
|
if (ImGui::BeginPopupModal("Import failed##URLImport", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
ImGui::Text("Failed to import patches from URL.");
|
|
if (!url_fetch_errors.empty()) {
|
|
ImGui::Text(url_fetch_errors.c_str());
|
|
}
|
|
ImGui::Separator();
|
|
if (ImGui::Button("OK")) {
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
// search function
|
|
if (!patches.empty()) {
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::DummyMarker();
|
|
ImGui::SameLine();
|
|
ImGui::SetNextItemWidth(300.f);
|
|
ImGui::InputTextWithHint(
|
|
"", "Type here to search..", &patch_name_filter,
|
|
ImGuiInputTextFlags_EscapeClearsAll);
|
|
if (!patch_name_filter.empty()) {
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Clear")) {
|
|
patch_name_filter.clear();
|
|
}
|
|
}
|
|
}
|
|
|
|
// check for empty list
|
|
if (patches.empty()) {
|
|
ImGui::TextColored(ImVec4(1.f, 0.f, 0.f, 1.f), "No patches available.");
|
|
ImGui::TextColored(ImVec4(1.f, 0.f, 0.f, 1.f), "New patches are no longer being added to spice2x.");
|
|
if (cfg::CONFIGURATOR_STANDALONE) {
|
|
ImGui::TextColored(ImVec4(1.f, 0.f, 0.f, 1.f), "Use Import button above to load patches from an online patcher.");
|
|
ImGui::TextColored(ImVec4(1.f, 0.f, 0.f, 1.f), "For more information, click the button below:");
|
|
if (ImGui::Button("More about patches")) {
|
|
// doing this on a separate thread to avoid polluting ImGui context
|
|
std::thread t([] {
|
|
ShellExecuteA(
|
|
NULL, "open",
|
|
"https://github.com/spice2x/spice2x.github.io/wiki/Patching-DLLs-(hex-edits)",
|
|
NULL, NULL, SW_SHOWNORMAL);
|
|
});
|
|
t.join();
|
|
}
|
|
} else {
|
|
ImGui::TextColored(ImVec4(1.f, 0.f, 0.f, 1.f), "Exit the game, launch spicecfg, and try importing patches from URL.");
|
|
}
|
|
} else {
|
|
// draw patches
|
|
if (ImGui::BeginTable("PatchesTable", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_RowBg)) {
|
|
ImGui::TableSetupColumn("##NameColumn", ImGuiTableColumnFlags_WidthStretch);
|
|
ImGui::TableSetupColumn("##OptionsColumn", ImGuiTableColumnFlags_WidthFixed, 240);
|
|
|
|
const auto search_str_in_lower = strtolower(patch_name_filter);
|
|
size_t patches_shown = 0;
|
|
for (auto &patch : patches) {
|
|
|
|
// get patch status
|
|
PatchStatus patch_status = is_patch_active(patch);
|
|
patch.last_status = patch_status;
|
|
|
|
// user requested to disable all
|
|
if (disable_all_patches && patch.enabled) {
|
|
patch.enabled = false;
|
|
config_dirty = true;
|
|
switch (patch_status) {
|
|
case PatchStatus::Enabled:
|
|
case PatchStatus::Disabled:
|
|
apply_patch(patch, false);
|
|
break;
|
|
case PatchStatus::Error:
|
|
if (cfg::CONFIGURATOR_STANDALONE) {
|
|
patch.enabled = false;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// search function
|
|
if (!patch_name_filter.empty()) {
|
|
if (patch.name_in_lower_case.find(search_str_in_lower) == std::string::npos) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// start drawing a row for this patch
|
|
ImGui::TableNextRow();
|
|
ImGui::PushID(&patch);
|
|
patches_shown += 1;
|
|
|
|
// first column, part 1: help / caution marker
|
|
ImGui::TableNextColumn();
|
|
const std::string description = patch.description;
|
|
const std::string caution = patch.caution;
|
|
if (!description.empty() && !caution.empty()) {
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::WarnMarker(description.c_str(), caution.c_str());
|
|
} else if (!description.empty()) {
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::HelpMarker(description.c_str());
|
|
} else if (!caution.empty()) {
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::WarnMarker(nullptr, caution.c_str());
|
|
} else {
|
|
ImGui::DummyMarker();
|
|
}
|
|
|
|
// get current state
|
|
bool patch_checked = patch_status == PatchStatus::Enabled;
|
|
|
|
// default text for the label (patch name)
|
|
auto patch_name = patch.name;
|
|
|
|
// push style
|
|
int style_color_pushed = 0;
|
|
switch (patch_status) {
|
|
case PatchStatus::Error:
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.f, 0.f, 0.f, 1.f));
|
|
patch_name += " (Error)";
|
|
style_color_pushed++;
|
|
break;
|
|
case PatchStatus::Enabled:
|
|
if (setting_auto_apply && patch.enabled) {
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.f, 1.f, 0.f, 1.f));
|
|
style_color_pushed++;
|
|
}
|
|
break;
|
|
case PatchStatus::Disabled:
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (patch.enabled) {
|
|
patch_name += setting_auto_apply ? " (Auto apply)" : " (Saved)";
|
|
}
|
|
if (patch.unverified) {
|
|
patch_name += " (Unverified patch)";
|
|
}
|
|
|
|
// first column, part 3: name
|
|
ImGui::SameLine();
|
|
ImGui::AlignTextToFramePadding();
|
|
// patch_name can include % (formatting markers) - ensure Unformatted widget used here
|
|
ImGui::TextUnformatted(patch_name.c_str());
|
|
if (style_color_pushed) {
|
|
ImGui::PopStyleColor(style_color_pushed);
|
|
}
|
|
if (patch.type == PatchType::Integer) {
|
|
ImGui::SameLine();
|
|
auto& numpatch = patch.patch_number;
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextDisabled("%d..%d", numpatch.min, numpatch.max);
|
|
}
|
|
|
|
// second column, part 1: enable checkbox (applies to all)
|
|
ImGui::TableNextColumn();
|
|
ImGui::BeginDisabled(patch_status == PatchStatus::Error);
|
|
if (ImGui::Checkbox("##patch_checked_checkbox", &patch_checked)) {
|
|
config_dirty = true;
|
|
switch (patch_status) {
|
|
case PatchStatus::Enabled:
|
|
case PatchStatus::Disabled:
|
|
if (patch_checked) {
|
|
setting_auto_apply = true;
|
|
}
|
|
patch.enabled = patch_checked;
|
|
apply_patch(patch, patch_checked);
|
|
break;
|
|
case PatchStatus::Error:
|
|
if (cfg::CONFIGURATOR_STANDALONE) {
|
|
if (patch_checked) {
|
|
setting_auto_apply = true;
|
|
}
|
|
patch.enabled = patch_checked;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
// update status
|
|
patch.last_status = is_patch_active(patch);
|
|
}
|
|
ImGui::EndDisabled();
|
|
|
|
// second column, part 2: additional options UI (dropdown, text input)
|
|
ImGui::SameLine();
|
|
if (patch_status == PatchStatus::Error){
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.f, 0.f, 0.f, 1.f));
|
|
if (patch.error_reason.empty()) {
|
|
ImGui::TextUnformatted("Unknown error");
|
|
} else {
|
|
ImGui::TextUnformatted(patch.error_reason.c_str());
|
|
}
|
|
ImGui::PopStyleColor();
|
|
} else if (patch.type == PatchType::Union || patch.type == PatchType::Integer) {
|
|
if (patch_status == PatchStatus::Enabled) {
|
|
if (patch.type == PatchType::Union) {
|
|
ImGui::SetNextItemWidth(200.0f);
|
|
if (ImGui::BeginCombo("##union_patch_dropdown", patch.selected_union_name.c_str())) {
|
|
for (const auto& union_patch : patch.patches_union) {
|
|
if (ImGui::Selectable(union_patch.name.c_str())) {
|
|
patch.selected_union_name = union_patch.name;
|
|
apply_patch(patch, true);
|
|
config_dirty = true;
|
|
}
|
|
}
|
|
ImGui::EndCombo();
|
|
}
|
|
} else if (patch.type == PatchType::Integer) {
|
|
ImGui::SetNextItemWidth(200.0f);
|
|
auto& numpatch = patch.patch_number;
|
|
ImGui::InputInt("##int_input", &numpatch.value, 1, 10);
|
|
if (ImGui::IsItemDeactivatedAfterEdit()) {
|
|
numpatch.value = CLAMP(
|
|
numpatch.value,
|
|
numpatch.min,
|
|
numpatch.max);
|
|
|
|
apply_patch(patch, true);
|
|
config_dirty = true;
|
|
}
|
|
}
|
|
} else if (patch_status == PatchStatus::Disabled) {
|
|
ImGui::SetNextItemWidth(200.0f);
|
|
ImGui::BeginDisabled();
|
|
if (patch.type == PatchType::Union) {
|
|
if (ImGui::BeginCombo(
|
|
"##dummy_union_patch_dropdown",
|
|
patch.selected_union_name.c_str())) {
|
|
ImGui::EndCombo();
|
|
}
|
|
} else if (patch.type == PatchType::Integer) {
|
|
ImGui::InputInt("##dummy_int_input", &patch.patch_number.value);
|
|
}
|
|
ImGui::EndDisabled();
|
|
}
|
|
} else {
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::BeginDisabled(!patch_checked);
|
|
ImGui::TextUnformatted(patch_checked ? "ON" : "off");
|
|
ImGui::EndDisabled();
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
|
|
if (patches_shown == 0) {
|
|
ImGui::TableNextRow();
|
|
ImGui::TableNextColumn();
|
|
ImGui::DummyMarker();
|
|
ImGui::SameLine();
|
|
ImGui::BeginDisabled();
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::TextUnformatted("No patches found.");
|
|
ImGui::EndDisabled();
|
|
ImGui::TableNextColumn();
|
|
ImGui::DummyMarker();
|
|
}
|
|
|
|
ImGui::EndTable();
|
|
}
|
|
}
|
|
}
|
|
|
|
void PatchManager::hard_apply_patches() {
|
|
std::vector<std::string> written_list;
|
|
for (auto& patch : patches) {
|
|
switch (patch.type) {
|
|
case PatchType::Memory:
|
|
for (auto& memory_patch : patch.patches_memory) {
|
|
auto dll_path = MODULE_PATH / memory_patch.dll_name;
|
|
create_dll_backup(written_list, dll_path);
|
|
auto dll_data = fileutils::bin_read(dll_path);
|
|
if (dll_data) {
|
|
auto max_len = std::max(memory_patch.data_disabled_len, memory_patch.data_enabled_len);
|
|
if (memory_patch.data_offset + max_len <= dll_data->size()) {
|
|
if (patch.enabled) {
|
|
memcpy(dll_data->data() + memory_patch.data_offset, memory_patch.data_enabled.get(), memory_patch.data_enabled_len);
|
|
} else {
|
|
memcpy(dll_data->data() + memory_patch.data_offset, memory_patch.data_disabled.get(), memory_patch.data_disabled_len);
|
|
}
|
|
fileutils::bin_write(dll_path, dll_data->data(), dll_data->size());
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case PatchType::Union:
|
|
if (!patch.enabled) {
|
|
break;
|
|
}
|
|
for (auto& union_patch : patch.patches_union) {
|
|
if (union_patch.name == patch.selected_union_name) {
|
|
auto dll_path = MODULE_PATH / union_patch.dll_name;
|
|
create_dll_backup(written_list, dll_path);
|
|
auto dll_data = fileutils::bin_read(dll_path);
|
|
if (dll_data) {
|
|
if (union_patch.offset + union_patch.data_len <= dll_data->size()) {
|
|
memcpy(dll_data->data() + union_patch.offset, union_patch.data.get(), union_patch.data_len);
|
|
fileutils::bin_write(dll_path, dll_data->data(), dll_data->size());
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
case PatchType::Integer:
|
|
{
|
|
if (!patch.enabled) {
|
|
break;
|
|
}
|
|
auto& numpatch = patch.patch_number;
|
|
auto dll_path = MODULE_PATH / numpatch.dll_name;
|
|
create_dll_backup(written_list, dll_path);
|
|
auto dll_data = fileutils::bin_read(dll_path);
|
|
if (dll_data) {
|
|
if (numpatch.data_offset + numpatch.size_in_bytes <= dll_data->size()) {
|
|
int_to_little_endian_bytes(
|
|
numpatch.value,
|
|
dll_data->data() + numpatch.data_offset,
|
|
numpatch.size_in_bytes);
|
|
fileutils::bin_write(dll_path, dll_data->data(), dll_data->size());
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void PatchManager::config_load() {
|
|
log_info("patchmanager", "loading config");
|
|
|
|
// read config file
|
|
std::string config = fileutils::text_read(config_path);
|
|
if (!config.empty()) {
|
|
|
|
// parse document
|
|
Document doc;
|
|
doc.Parse(config.c_str());
|
|
|
|
// check parse error
|
|
auto error = doc.GetParseError();
|
|
if (error) {
|
|
log_warning("patchmanager", "config file parse error: {}", error);
|
|
}
|
|
|
|
// verify root is a dict
|
|
if (doc.IsObject()) {
|
|
|
|
// read auto apply settings
|
|
auto auto_apply = doc.FindMember("auto_apply");
|
|
if (auto_apply != doc.MemberEnd() && auto_apply->value.IsArray()) {
|
|
|
|
// get game id
|
|
auto game_id = avs::game::get_identifier();
|
|
|
|
// iterate entries
|
|
setting_auto_apply = false;
|
|
setting_auto_apply_list.clear();
|
|
for (auto &entry : auto_apply->value.GetArray()) {
|
|
if (entry.IsString()) {
|
|
|
|
// check if this is our game identifier
|
|
std::string entry_id = entry.GetString();
|
|
if (game_id == entry_id) {
|
|
setting_auto_apply = true;
|
|
}
|
|
|
|
// move to list
|
|
setting_auto_apply_list.emplace_back(entry_id);
|
|
}
|
|
}
|
|
}
|
|
|
|
// read enabled patches
|
|
auto patches_enabled = doc.FindMember("patches_enabled");
|
|
if (patches_enabled != doc.MemberEnd() && patches_enabled->value.IsArray()) {
|
|
setting_patches_enabled.clear();
|
|
for (const auto &patch : patches_enabled->value.GetArray()) {
|
|
if (patch.IsString()) {
|
|
setting_patches_enabled.emplace_back(std::string(patch.GetString()));
|
|
}
|
|
}
|
|
}
|
|
// read enabled union patches
|
|
auto patches_union_enabled = doc.FindMember("union_patches_enabled");
|
|
if (patches_union_enabled != doc.MemberEnd() && patches_union_enabled->value.IsObject()) {
|
|
setting_union_patches_enabled.clear();
|
|
for (auto it = patches_union_enabled->value.MemberBegin(); it != patches_union_enabled->value.MemberEnd(); ++it) {
|
|
if (it->name.IsString() && it->value.IsString()) {
|
|
setting_union_patches_enabled[it->name.GetString()] = it->value.GetString();
|
|
}
|
|
}
|
|
}
|
|
// read enabled integer patches
|
|
auto patches_int_enabled = doc.FindMember("integer_patches_enabled");
|
|
if (patches_int_enabled != doc.MemberEnd() && patches_int_enabled->value.IsObject()) {
|
|
setting_int_patches_enabled.clear();
|
|
for (auto it = patches_int_enabled->value.MemberBegin(); it != patches_int_enabled->value.MemberEnd(); ++it) {
|
|
if (it->name.IsString() && it->value.IsNumber()) {
|
|
setting_int_patches_enabled[it->name.GetString()] = it->value.GetInt();
|
|
}
|
|
}
|
|
}
|
|
|
|
// read remote patch URLs
|
|
auto remote_url_history = doc.FindMember("remote_url_history");
|
|
if (remote_url_history != doc.MemberEnd() && remote_url_history->value.IsArray()) {
|
|
url_recents.clear();
|
|
for (const auto &url : remote_url_history->value.GetArray()) {
|
|
if (url.IsString()) {
|
|
url_recents.emplace_back(std::string(url.GetString()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static std::string patch_hash(PatchData &patch) {
|
|
SHA256 hash;
|
|
hash.add(patch.game_code.c_str(), patch.game_code.length());
|
|
if (patch.datecode_min != 0 || patch.datecode_max != 0) {
|
|
hash.add(&patch.datecode_min, sizeof(patch.datecode_min));
|
|
hash.add(&patch.datecode_max, sizeof(patch.datecode_max));
|
|
}
|
|
if (!patch.peIdentifier.empty()) {
|
|
hash.add(patch.peIdentifier.c_str(), patch.peIdentifier.length());
|
|
}
|
|
hash.add(patch.name.c_str(), patch.name.length());
|
|
hash.add(patch.description.c_str(), patch.description.length());
|
|
return hash.getHash();
|
|
}
|
|
|
|
void PatchManager::config_save() {
|
|
|
|
// create document
|
|
Document doc;
|
|
doc.Parse(
|
|
"{"
|
|
" \"auto_apply\": [],"
|
|
" \"patches_enabled\": [],"
|
|
" \"union_patches_enabled\": {},"
|
|
" \"integer_patches_enabled\": {},"
|
|
" \"remote_url_history\": []"
|
|
"}"
|
|
);
|
|
|
|
// check parse error
|
|
auto error = doc.GetParseError();
|
|
if (error) {
|
|
log_warning("patchmanager", "template parse error: {}", error);
|
|
}
|
|
|
|
// auto apply setting
|
|
auto &auto_apply_list = doc["auto_apply"];
|
|
auto game_id = avs::game::get_identifier();
|
|
bool game_id_added = false;
|
|
for (auto &entry : setting_auto_apply_list) {
|
|
if (entry == game_id) {
|
|
if (!setting_auto_apply) {
|
|
continue;
|
|
}
|
|
game_id_added = true;
|
|
}
|
|
auto_apply_list.PushBack(StringRef(entry.c_str()), doc.GetAllocator());
|
|
}
|
|
if (setting_auto_apply && !game_id_added) {
|
|
auto_apply_list.PushBack(StringRef(game_id.c_str()), doc.GetAllocator());
|
|
}
|
|
|
|
// get enabled patches
|
|
auto &doc_patches_enabled = doc["patches_enabled"];
|
|
auto &doc_union_patches_enable = doc["union_patches_enabled"];
|
|
auto &doc_int_patches_enable = doc["integer_patches_enabled"];
|
|
for (auto &patch : patches) {
|
|
auto hash = patch_hash(patch);
|
|
|
|
if (patch.type == PatchType::Union) {
|
|
// enable hash if known as enabled, overridden and missing from list
|
|
if (patch.enabled) {
|
|
setting_union_patches_enabled[hash] = patch.selected_union_name;
|
|
} else {
|
|
setting_union_patches_enabled.erase(hash);
|
|
}
|
|
} else if (patch.type == PatchType::Integer) {
|
|
if (patch.enabled) {
|
|
setting_int_patches_enabled[hash] = patch.patch_number.value;
|
|
} else {
|
|
setting_int_patches_enabled.erase(hash);
|
|
}
|
|
} else {
|
|
// hash patch and find entry
|
|
auto entry = std::find(setting_patches_enabled.begin(), setting_patches_enabled.end(), hash);
|
|
|
|
// enable hash if known as enabled, overridden and missing from list
|
|
if ((patch.last_status == PatchStatus::Enabled && patch.enabled)
|
|
|| (cfg::CONFIGURATOR_STANDALONE && patch.last_status == PatchStatus::Error && patch.enabled)) {
|
|
if (entry == setting_patches_enabled.end()) {
|
|
setting_patches_enabled.emplace_back(hash);
|
|
}
|
|
}
|
|
|
|
// disable hash if patch known as disabled
|
|
if (patch.last_status == PatchStatus::Disabled
|
|
|| (cfg::CONFIGURATOR_STANDALONE && patch.last_status == PatchStatus::Error && !patch.enabled)) {
|
|
if (entry != setting_patches_enabled.end()) {
|
|
setting_patches_enabled.erase(entry);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// add hashes to document
|
|
for (auto &hash : setting_patches_enabled) {
|
|
Value hash_value(hash.c_str(), doc.GetAllocator());
|
|
doc_patches_enabled.PushBack(hash_value, doc.GetAllocator());
|
|
}
|
|
|
|
for (auto& it : setting_union_patches_enabled) {
|
|
const std::string& key = it.first;
|
|
const std::string& val = it.second;
|
|
doc_union_patches_enable.AddMember(StringRef(key.c_str()), StringRef(val.c_str()), doc.GetAllocator());
|
|
}
|
|
|
|
for (auto& it : setting_int_patches_enabled) {
|
|
const std::string& key = it.first;
|
|
const int32_t& val = it.second;
|
|
doc_int_patches_enable.AddMember(StringRef(key.c_str()), val, doc.GetAllocator());
|
|
}
|
|
|
|
// remote URLs
|
|
auto &doc_url_history = doc["remote_url_history"];
|
|
for (auto& url : url_recents) {
|
|
Value url_value(url.c_str(), doc.GetAllocator());
|
|
doc_url_history.PushBack(url_value, doc.GetAllocator());
|
|
}
|
|
|
|
// build JSON
|
|
StringBuffer buffer;
|
|
PrettyWriter<StringBuffer> writer(buffer);
|
|
doc.Accept(writer);
|
|
|
|
// save to file
|
|
if (fileutils::text_write(config_path, buffer.GetString())) {
|
|
config_dirty = false;
|
|
} else {
|
|
log_warning("patchmanager", "unable to save config file to {}", config_path.string());
|
|
}
|
|
}
|
|
|
|
std::string get_game_identifier(const std::filesystem::path& dll_path) {
|
|
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);
|
|
|
|
return identifier;
|
|
}
|
|
|
|
void PatchManager::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: {}", error);
|
|
}
|
|
|
|
// iterate patches
|
|
for (auto &patch : doc.GetArray()) {
|
|
|
|
// 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;
|
|
}
|
|
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_it->value.GetString(),
|
|
.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(),
|
|
.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: {}", patch_data.type);
|
|
break;
|
|
}
|
|
|
|
// auto apply
|
|
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 PatchManager::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()) {
|
|
if (!fileutils::dir_exists(LOCAL_PATCHES_PATH))
|
|
fileutils::dir_create(LOCAL_PATCHES_PATH);
|
|
std::filesystem::path save_path = LOCAL_PATCHES_PATH / (identifier + ".json");
|
|
fileutils::text_write(save_path, patches_json);
|
|
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());
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool PatchManager::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.string());
|
|
continue;
|
|
}
|
|
|
|
log_misc("patchmanager", "reading from patches.json: {}", patches_json_path.string());
|
|
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.string());
|
|
if (0 < new_patches) {
|
|
ret = true;
|
|
ACTIVE_JSON_FILE = patches_json_path.string();
|
|
break;
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
void PatchManager::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();
|
|
if (cfg::CONFIGURATOR_STANDALONE) {
|
|
DLL_MAP.clear();
|
|
DLL_MAP_ORG.clear();
|
|
}
|
|
|
|
ACTIVE_JSON_FILE = "";
|
|
|
|
std::string firstDll = avs::game::DLL_NAME;
|
|
std::string first_id = get_game_identifier(MODULE_PATH / firstDll);
|
|
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);
|
|
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.string());
|
|
std::string content = fileutils::text_read(firstPath);
|
|
append_patches(content, apply_patches, nullptr, first_id);
|
|
ACTIVE_JSON_FILE = firstPath.string();
|
|
}
|
|
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.string());
|
|
std::string content = fileutils::text_read(extraPath);
|
|
append_patches(content, apply_patches, nullptr, extraId);
|
|
if (ACTIVE_JSON_FILE.empty()) {
|
|
ACTIVE_JSON_FILE = extraPath.string();
|
|
} else {
|
|
ACTIVE_JSON_FILE += ", " + extraPath.string();
|
|
}
|
|
}
|
|
} 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 PatchManager::import_remote_patches_to_disk() {
|
|
bool imported = false;
|
|
// clear old patches
|
|
patches.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 PatchManager::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,
|
|
error,
|
|
rapidjson::GetParseError_En(error));
|
|
}
|
|
|
|
// iterate patches
|
|
for (auto &patch : doc.GetArray()) {
|
|
|
|
// 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;
|
|
}
|
|
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_it->value.GetString(),
|
|
.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(),
|
|
.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: {}", patch_data.type);
|
|
break;
|
|
}
|
|
|
|
// auto apply
|
|
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);
|
|
}
|
|
}
|
|
|
|
PatchStatus is_patch_active(PatchData &patch) {
|
|
|
|
// check patch type
|
|
switch (patch.type) {
|
|
case PatchType::Memory: {
|
|
|
|
// iterate patches
|
|
bool enabled = false;
|
|
bool disabled = false;
|
|
for (auto &memory_patch : patch.patches_memory) {
|
|
auto max_size = std::max(memory_patch.data_enabled_len, memory_patch.data_disabled_len);
|
|
|
|
// check for error to not try to get the pointer every frame
|
|
if (memory_patch.fatal_error) {
|
|
if (cfg::CONFIGURATOR_STANDALONE) {
|
|
patch.unverified = true;
|
|
return patch.enabled ? PatchStatus::Enabled : PatchStatus::Disabled;
|
|
}
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// find data pointer if not known yet
|
|
if (memory_patch.data_offset_ptr == nullptr) {
|
|
// check if file exists
|
|
auto dll_path = MODULE_PATH / memory_patch.dll_name;
|
|
if (!fileutils::file_exists(dll_path)) {
|
|
// file does not exist so that's pretty fatal
|
|
memory_patch.fatal_error = true;
|
|
patch.error_reason = "DLL not found on disk";
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// standalone mode
|
|
if (cfg::CONFIGURATOR_STANDALONE) {
|
|
auto file = find_in_dll_map(
|
|
memory_patch.dll_name, memory_patch.data_offset, max_size);
|
|
if (!file) {
|
|
patch.error_reason = "Invalid DLL or offset";
|
|
return PatchStatus::Error;
|
|
}
|
|
memory_patch.data_offset_ptr = &(*file)[memory_patch.data_offset];
|
|
|
|
} else {
|
|
// get module
|
|
auto module = libutils::try_module(dll_path);
|
|
if (!module) {
|
|
// no fatal error, might just not be loaded yet
|
|
patch.error_reason = "DLL not loaded into memory";
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// convert offset to RVA
|
|
auto offset = libutils::offset2rva(dll_path, memory_patch.data_offset);
|
|
if (offset == -1) {
|
|
// RVA not found means unrecoverable
|
|
memory_patch.fatal_error = true;
|
|
patch.error_reason = "RVA not found";
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// get module information
|
|
MODULEINFO module_info {};
|
|
if (!GetModuleInformation(
|
|
GetCurrentProcess(),
|
|
module,
|
|
&module_info,
|
|
sizeof(MODULEINFO))) {
|
|
// hmm, maybe try again sometime, not fatal
|
|
patch.error_reason = "Failed to get module info";
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// check bounds
|
|
auto max_offset = static_cast<uintptr_t>(offset) + max_size;
|
|
auto image_size = static_cast<uintptr_t>(module_info.SizeOfImage);
|
|
if (max_offset >= image_size) {
|
|
// outside of bounds, invalid patch, fatal
|
|
memory_patch.fatal_error = true;
|
|
patch.error_reason = "Invalid DLL or offset";
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// save pointer
|
|
auto dll_base = reinterpret_cast<uintptr_t>(module_info.lpBaseOfDll);
|
|
memory_patch.data_offset_ptr = reinterpret_cast<uint8_t *>(dll_base + offset);
|
|
}
|
|
}
|
|
|
|
// virtual protect
|
|
memutils::VProtectGuard guard(memory_patch.data_offset_ptr, max_size);
|
|
|
|
// compare
|
|
if (!guard.is_bad_address() && !memcmp(
|
|
memory_patch.data_enabled.get(),
|
|
memory_patch.data_offset_ptr,
|
|
memory_patch.data_enabled_len)) {
|
|
enabled = true;
|
|
} else if (!guard.is_bad_address() && !memcmp(
|
|
memory_patch.data_disabled.get(),
|
|
memory_patch.data_offset_ptr,
|
|
memory_patch.data_disabled_len)) {
|
|
disabled = true;
|
|
} else {
|
|
patch.error_reason = "Bad patch; patch is neither on or off (single patch)";
|
|
return PatchStatus::Error;
|
|
}
|
|
}
|
|
// check detection flags
|
|
if (enabled && disabled) {
|
|
patch.error_reason = "Bad patch; patch is both on and off (cumulative)";
|
|
return PatchStatus::Error;
|
|
} else if (enabled) {
|
|
return PatchStatus::Enabled;
|
|
} else if (disabled) {
|
|
return PatchStatus::Disabled;
|
|
} else {
|
|
patch.error_reason = "Bad patch; patch is neither on or off (cumulative)";
|
|
return PatchStatus::Error;
|
|
}
|
|
}
|
|
case PatchType::Signature: {
|
|
return PatchStatus::Error;
|
|
}
|
|
case PatchType::Union: {
|
|
// iterate patches
|
|
bool match_found = false;
|
|
patch.selected_union_name = "";
|
|
for (auto &union_patch : patch.patches_union) {
|
|
// check for error to not try to get the pointer every frame
|
|
if (union_patch.fatal_error) {
|
|
if (cfg::CONFIGURATOR_STANDALONE) {
|
|
patch.unverified = true;
|
|
return patch.enabled ? PatchStatus::Enabled : PatchStatus::Disabled;
|
|
}
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// find data pointer if not known yet
|
|
if (union_patch.data_offset_ptr == nullptr) {
|
|
// check if file exists
|
|
auto dll_path = MODULE_PATH / union_patch.dll_name;
|
|
if (!fileutils::file_exists(dll_path)) {
|
|
// file does not exist so that's pretty fatal
|
|
union_patch.fatal_error = true;
|
|
patch.error_reason = "DLL not found on disk";
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// standalone mode
|
|
if (cfg::CONFIGURATOR_STANDALONE) {
|
|
auto file = find_in_dll_map(
|
|
union_patch.dll_name, union_patch.offset, union_patch.data_len);
|
|
if (!file) {
|
|
patch.error_reason = "Invalid DLL or offset";
|
|
return PatchStatus::Error;
|
|
}
|
|
union_patch.data_offset_ptr = &(*file)[union_patch.offset];
|
|
|
|
} else {
|
|
// get module
|
|
auto module = libutils::try_module(dll_path);
|
|
if (!module) {
|
|
// no fatal error, might just not be loaded yet
|
|
patch.error_reason = "DLL not loaded into memory";
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// convert offset to RVA
|
|
auto offset = libutils::offset2rva(dll_path, union_patch.offset);
|
|
if (offset == -1) {
|
|
// RVA not found means unrecoverable
|
|
union_patch.fatal_error = true;
|
|
patch.error_reason = "RVA not found";
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// get module information
|
|
MODULEINFO module_info {};
|
|
if (!GetModuleInformation(
|
|
GetCurrentProcess(),
|
|
module,
|
|
&module_info,
|
|
sizeof(MODULEINFO))) {
|
|
// hmm, maybe try again sometime, not fatal
|
|
patch.error_reason = "Failed to get module info";
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// check bounds
|
|
auto max_offset = static_cast<uintptr_t>(offset) + union_patch.data_len;
|
|
auto image_size = static_cast<uintptr_t>(module_info.SizeOfImage);
|
|
if (max_offset >= image_size) {
|
|
// outside of bounds, invalid patch, fatal
|
|
union_patch.fatal_error = true;
|
|
patch.error_reason = "Invalid DLL or offset";
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// save pointer
|
|
auto dll_base = reinterpret_cast<uintptr_t>(module_info.lpBaseOfDll);
|
|
union_patch.data_offset_ptr = reinterpret_cast<uint8_t *>(dll_base + offset);
|
|
}
|
|
}
|
|
|
|
// virtual protect
|
|
memutils::VProtectGuard guard(union_patch.data_offset_ptr, union_patch.data_len);
|
|
if (guard.is_bad_address()) {
|
|
patch.error_reason = "Invalid offset, bad address";
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// is this union patch enabled in DLL?
|
|
if (!match_found &&
|
|
memcmp(union_patch.data.get(), union_patch.data_offset_ptr, union_patch.data_len) == 0) {
|
|
match_found = true;
|
|
patch.selected_union_name = union_patch.name;
|
|
}
|
|
|
|
// if everything is OK, continue to check other patches in this union
|
|
}
|
|
// none of the union patches match what's in the DLL
|
|
if (!match_found) {
|
|
patch.error_reason = "No match found in union";
|
|
return PatchStatus::Error;
|
|
}
|
|
return patch.enabled ? PatchStatus::Enabled : PatchStatus::Disabled;
|
|
}
|
|
case PatchType::Integer: {
|
|
auto& numpatch = patch.patch_number;
|
|
numpatch.value = 0;
|
|
|
|
// check for fatal error and give up early
|
|
if (numpatch.fatal_error) {
|
|
if (cfg::CONFIGURATOR_STANDALONE) {
|
|
patch.unverified = true;
|
|
return patch.enabled ? PatchStatus::Enabled : PatchStatus::Disabled;
|
|
}
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// find data pointer if not known yet
|
|
if (numpatch.data_offset_ptr == nullptr) {
|
|
// check if file exists
|
|
auto dll_path = MODULE_PATH / numpatch.dll_name;
|
|
if (!fileutils::file_exists(dll_path)) {
|
|
// file does not exist so that's pretty fatal
|
|
numpatch.fatal_error = true;
|
|
patch.error_reason = "DLL not found on disk";
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// standalone mode
|
|
if (cfg::CONFIGURATOR_STANDALONE) {
|
|
const auto file = find_in_dll_map(
|
|
numpatch.dll_name, numpatch.data_offset, numpatch.size_in_bytes);
|
|
if (!file) {
|
|
patch.error_reason = "Invalid DLL or offset";
|
|
return PatchStatus::Error;
|
|
}
|
|
numpatch.data_offset_ptr = &(*file)[numpatch.data_offset];
|
|
} else {
|
|
// get module
|
|
const auto module = libutils::try_module(dll_path);
|
|
if (!module) {
|
|
// no fatal error, might just not be loaded yet
|
|
patch.error_reason = "DLL not loaded into memory";
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// convert offset to RVA
|
|
const auto offset = libutils::offset2rva(dll_path, numpatch.data_offset);
|
|
if (offset == -1) {
|
|
// RVA not found means unrecoverable
|
|
numpatch.fatal_error = true;
|
|
patch.error_reason = "RVA not found";
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// get module information
|
|
MODULEINFO module_info {};
|
|
if (!GetModuleInformation(
|
|
GetCurrentProcess(),
|
|
module,
|
|
&module_info,
|
|
sizeof(MODULEINFO))) {
|
|
|
|
// hmm, maybe try again sometime, not fatal
|
|
patch.error_reason = "Failed to get module info";
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// check bounds
|
|
const auto max_offset = static_cast<uintptr_t>(offset) + numpatch.size_in_bytes;
|
|
const auto image_size = static_cast<uintptr_t>(module_info.SizeOfImage);
|
|
if (max_offset >= image_size) {
|
|
// outside of bounds, invalid patch, fatal
|
|
numpatch.fatal_error = true;
|
|
patch.error_reason = "Invalid DLL or offset";
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// save pointer so we don't have to do this again next frame
|
|
const auto dll_base = reinterpret_cast<uintptr_t>(module_info.lpBaseOfDll);
|
|
numpatch.data_offset_ptr = reinterpret_cast<uint8_t *>(dll_base + offset);
|
|
}
|
|
}
|
|
|
|
// virtual protect
|
|
memutils::VProtectGuard guard(numpatch.data_offset_ptr, numpatch.size_in_bytes);
|
|
if (guard.is_bad_address()) {
|
|
patch.error_reason = "Invalid offset, bad address";
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// what is the current value? check bounds
|
|
const auto value_in_dll =
|
|
parse_little_endian_int(numpatch.data_offset_ptr, numpatch.size_in_bytes);
|
|
if (value_in_dll < numpatch.min || numpatch.max < value_in_dll) {
|
|
patch.error_reason = "Number out of range, check min/max";
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
patch.patch_number.value = value_in_dll;
|
|
return patch.enabled ? PatchStatus::Enabled : PatchStatus::Disabled;
|
|
}
|
|
case PatchType::Unknown:
|
|
default:
|
|
patch.error_reason = "Unknown patch type";
|
|
return PatchStatus::Error;
|
|
}
|
|
}
|
|
|
|
bool apply_patch(PatchData &patch, bool active) {
|
|
|
|
// check patch type
|
|
switch (patch.type) {
|
|
case PatchType::Memory: {
|
|
|
|
// iterate memory patches
|
|
for (auto &memory_patch : patch.patches_memory) {
|
|
|
|
/*
|
|
* we won't use the cached data_offset_ptr here
|
|
* that makes it more reliable, also only happens on load/toggle
|
|
*/
|
|
|
|
// determine source/target buffer/size
|
|
uint8_t *src_buf = active
|
|
? memory_patch.data_disabled.get()
|
|
: memory_patch.data_enabled.get();
|
|
size_t src_len = active
|
|
? memory_patch.data_disabled_len
|
|
: memory_patch.data_enabled_len;
|
|
uint8_t *target_buf = active
|
|
? memory_patch.data_enabled.get()
|
|
: memory_patch.data_disabled.get();
|
|
size_t target_len = active
|
|
? memory_patch.data_enabled_len
|
|
: memory_patch.data_disabled_len;
|
|
|
|
// standalone mode
|
|
if (cfg::CONFIGURATOR_STANDALONE) {
|
|
auto max_len = std::max(src_len, target_len);
|
|
// find file from DLL_MAP
|
|
auto dll_file = find_in_dll_map(
|
|
memory_patch.dll_name, memory_patch.data_offset, max_len);
|
|
if (!dll_file) {
|
|
return false;
|
|
}
|
|
// find offset into file
|
|
if (memory_patch.data_offset_ptr == nullptr) {
|
|
memory_patch.data_offset_ptr =
|
|
&(*dll_file)[memory_patch.data_offset];
|
|
}
|
|
if (memory_patch.data_offset_ptr == nullptr) {
|
|
return false;
|
|
}
|
|
// copy target to memory if src matches
|
|
if (memcmp(memory_patch.data_offset_ptr, src_buf, src_len) == 0) {
|
|
memcpy(memory_patch.data_offset_ptr, target_buf, target_len);
|
|
}
|
|
|
|
} else {
|
|
|
|
// get pointer to offset
|
|
auto max_len = std::max(src_len, target_len);
|
|
if (memory_patch.data_offset_ptr == nullptr) {
|
|
memory_patch.data_offset_ptr =
|
|
get_dll_offset_for_patch_apply(
|
|
memory_patch.dll_name,
|
|
memory_patch.data_offset,
|
|
max_len);
|
|
if (memory_patch.data_offset_ptr == nullptr) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// virtual protect
|
|
memutils::VProtectGuard guard(
|
|
memory_patch.data_offset_ptr, max_len);
|
|
|
|
// copy target to memory if src matches
|
|
if (memcmp(memory_patch.data_offset_ptr, src_buf, src_len) == 0) {
|
|
memcpy(memory_patch.data_offset_ptr, target_buf, target_len);
|
|
}
|
|
}
|
|
}
|
|
|
|
// success
|
|
return true;
|
|
}
|
|
case PatchType::Signature: {
|
|
return false;
|
|
}
|
|
case PatchType::Union: {
|
|
// Find the selected union patch
|
|
auto it = std::find_if(patch.patches_union.begin(), patch.patches_union.end(),
|
|
[&](const UnionPatch& up) { return up.name == patch.selected_union_name; });
|
|
if (it == patch.patches_union.end()) {
|
|
return false;
|
|
}
|
|
auto& union_patch = *it;
|
|
|
|
// find data_offset_ptr
|
|
if (cfg::CONFIGURATOR_STANDALONE) {
|
|
// find file from DLL_MAP
|
|
auto dll_file = find_in_dll_map(
|
|
union_patch.dll_name, union_patch.offset, union_patch.data_len);
|
|
if (!dll_file) {
|
|
return false;
|
|
}
|
|
// find offset into file
|
|
if (union_patch.data_offset_ptr == nullptr) {
|
|
union_patch.data_offset_ptr =
|
|
reinterpret_cast<uint8_t*>(union_patch.offset + &(*dll_file)[0]);
|
|
}
|
|
} else {
|
|
if (union_patch.data_offset_ptr == nullptr) {
|
|
union_patch.data_offset_ptr = get_dll_offset_for_patch_apply(
|
|
union_patch.dll_name,
|
|
union_patch.offset,
|
|
union_patch.data_len);
|
|
}
|
|
}
|
|
|
|
if (union_patch.data_offset_ptr == nullptr) {
|
|
return false;
|
|
}
|
|
|
|
// Apply the selected union patch
|
|
memutils::VProtectGuard guard(union_patch.data_offset_ptr, union_patch.data_len);
|
|
if (active) {
|
|
// apply the selected patch
|
|
memcpy(union_patch.data_offset_ptr, union_patch.data.get(), union_patch.data_len);
|
|
return true;
|
|
} else {
|
|
// restore from original file on disk
|
|
return restore_bytes_from_dll_map_org(
|
|
union_patch.data_offset_ptr,
|
|
union_patch.dll_name,
|
|
union_patch.offset,
|
|
union_patch.data_len);
|
|
}
|
|
}
|
|
case PatchType::Integer: {
|
|
auto& numpatch = patch.patch_number;
|
|
if (cfg::CONFIGURATOR_STANDALONE) {
|
|
// find file from DLL_MAP
|
|
auto dll_file = find_in_dll_map(
|
|
numpatch.dll_name, numpatch.data_offset, numpatch.size_in_bytes);
|
|
if (!dll_file) {
|
|
return false;
|
|
}
|
|
// find offset into file
|
|
if (numpatch.data_offset_ptr == nullptr) {
|
|
numpatch.data_offset_ptr =
|
|
reinterpret_cast<uint8_t*>(numpatch.data_offset + &(*dll_file)[0]);
|
|
}
|
|
} else {
|
|
if (numpatch.data_offset_ptr == nullptr) {
|
|
numpatch.data_offset_ptr = get_dll_offset_for_patch_apply(
|
|
numpatch.dll_name,
|
|
numpatch.data_offset,
|
|
numpatch.size_in_bytes);
|
|
}
|
|
}
|
|
if (numpatch.data_offset_ptr == nullptr) {
|
|
return false;
|
|
}
|
|
memutils::VProtectGuard guard(numpatch.data_offset_ptr, numpatch.size_in_bytes);
|
|
if (active) {
|
|
// apply the selected patch
|
|
int_to_little_endian_bytes(
|
|
numpatch.value, numpatch.data_offset_ptr, numpatch.size_in_bytes);
|
|
return true;
|
|
} else {
|
|
// restore from original file on disk
|
|
return restore_bytes_from_dll_map_org(
|
|
numpatch.data_offset_ptr,
|
|
numpatch.dll_name,
|
|
numpatch.data_offset,
|
|
numpatch.size_in_bytes);
|
|
}
|
|
return false;
|
|
}
|
|
default: {
|
|
|
|
// unknown patch type - fail
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
MemoryPatch SignaturePatch::to_memory(PatchData *patch) {
|
|
|
|
// check if file exists
|
|
auto dll_path = MODULE_PATH / dll_name;
|
|
if (!fileutils::file_exists(dll_path)) {
|
|
|
|
// file does not exist so that's pretty fatal
|
|
return {.fatal_error = true};
|
|
}
|
|
|
|
// remove spaces
|
|
signature.erase(std::remove(signature.begin(), signature.end(), ' '), signature.end());
|
|
replacement.erase(std::remove(replacement.begin(), replacement.end(), ' '), replacement.end());
|
|
|
|
// build pattern
|
|
std::string pattern_str(signature);
|
|
strreplace(pattern_str, "??", "00");
|
|
strreplace(pattern_str, "XX", "00");
|
|
auto pattern_bin = std::make_unique<uint8_t[]>(signature.length() / 2);
|
|
if (!hex2bin(pattern_str.c_str(), pattern_bin.get())) {
|
|
return {.fatal_error = true};
|
|
}
|
|
|
|
// build signature mask
|
|
std::ostringstream signature_mask;
|
|
for (size_t i = 0; i < signature.length(); i += 2) {
|
|
if (signature[i] == '?' || signature[i] == 'X') {
|
|
if (signature[i + 1] == '?' || signature[i + 1] == 'X') {
|
|
signature_mask << '?';
|
|
} else {
|
|
return {.fatal_error = true};
|
|
}
|
|
} else {
|
|
signature_mask << 'X';
|
|
}
|
|
}
|
|
std::string signature_mask_str = signature_mask.str();
|
|
|
|
// build replace data
|
|
std::string replace_data_str(replacement);
|
|
strreplace(replace_data_str, "??", "00");
|
|
strreplace(replace_data_str, "XX", "00");
|
|
auto replace_data_bin = std::make_unique<uint8_t[]>(replacement.length() / 2);
|
|
if (!hex2bin(replace_data_str.c_str(), replace_data_bin.get())) {
|
|
return {.fatal_error = true};
|
|
}
|
|
|
|
// build replace mask
|
|
std::ostringstream replace_mask;
|
|
for (size_t i = 0; i < replacement.length(); i += 2) {
|
|
if (replacement[i] == '?' || replacement[i] == 'X') {
|
|
if (replacement[i + 1] == '?' || replacement[i + 1] == 'X') {
|
|
replace_mask << '?';
|
|
} else {
|
|
return {.fatal_error = true};
|
|
}
|
|
} else {
|
|
replace_mask << 'X';
|
|
}
|
|
}
|
|
std::string replace_mask_str = replace_mask.str();
|
|
|
|
// find offset
|
|
uint64_t data_offset = 0;
|
|
uint8_t *data_offset_ptr = nullptr;
|
|
uintptr_t data_offset_ptr_base = 0;
|
|
if (cfg::CONFIGURATOR_STANDALONE) {
|
|
|
|
// load file into dll map if missing
|
|
auto it = DLL_MAP.find(dll_name);
|
|
if (it == DLL_MAP.end()) {
|
|
DLL_MAP[dll_name] =
|
|
std::unique_ptr<std::vector<uint8_t>>(
|
|
fileutils::bin_read(dll_path));
|
|
it = DLL_MAP.find(dll_name);
|
|
}
|
|
|
|
// find pattern
|
|
data_offset = find_pattern(*it->second, 0, pattern_bin.get(), signature_mask_str.c_str(), offset, usage);
|
|
data_offset_ptr = reinterpret_cast<uint8_t *>(data_offset);
|
|
data_offset_ptr_base = (uintptr_t) it->second->data();
|
|
|
|
} else {
|
|
|
|
// get module
|
|
auto module = libutils::try_module(dll_path);
|
|
bool module_free = false;
|
|
if (!module) {
|
|
module = libutils::try_library(dll_path);
|
|
if (module) {
|
|
module_free = true;
|
|
} else {
|
|
return {.fatal_error = true};
|
|
}
|
|
}
|
|
|
|
// find pattern
|
|
data_offset_ptr = reinterpret_cast<uint8_t *>(
|
|
find_pattern(module, pattern_bin.get(), signature_mask_str.c_str(), offset, usage));
|
|
|
|
// convert back to offset
|
|
data_offset = libutils::rva2offset(dll_path, (intptr_t) (data_offset_ptr - (uint8_t*) module));
|
|
|
|
// clean
|
|
if (module_free) {
|
|
FreeLibrary(module);
|
|
}
|
|
}
|
|
|
|
// check pointers
|
|
if (data_offset_ptr == nullptr) {
|
|
return {.fatal_error = true};
|
|
}
|
|
|
|
// get disabled/enabled data
|
|
size_t data_len = std::max(signature_mask_str.length(), replace_mask_str.length());
|
|
std::shared_ptr<uint8_t[]> data_disabled(new uint8_t[data_len]);
|
|
std::shared_ptr<uint8_t[]> data_enabled(new uint8_t[data_len]);
|
|
memutils::VProtectGuard data_guard(data_offset_ptr + data_offset_ptr_base, data_len);
|
|
for (size_t i = 0; i < data_len; ++i) {
|
|
if (i >= signature_mask_str.length() || signature_mask_str[i] != 'X') {
|
|
data_disabled.get()[i] = (data_offset_ptr + data_offset_ptr_base)[i];
|
|
} else {
|
|
data_disabled.get()[i] = pattern_bin.get()[i];
|
|
}
|
|
}
|
|
for (size_t i = 0; i < data_len; ++i) {
|
|
if (i >= replace_mask_str.length() || replace_mask_str[i] != 'X') {
|
|
data_enabled.get()[i] = (data_offset_ptr + data_offset_ptr_base)[i];
|
|
} else {
|
|
data_enabled.get()[i] = replace_data_bin.get()[i];
|
|
}
|
|
}
|
|
|
|
// log edit
|
|
log_misc("patchmanager", "found {}: {:#08X}: {} -> {}",
|
|
patch->name, data_offset,
|
|
bin2hex(data_disabled.get(), data_len),
|
|
bin2hex(data_enabled.get(), data_len));
|
|
|
|
// build patch
|
|
return MemoryPatch {
|
|
.dll_name = dll_name,
|
|
.data_disabled = std::move(data_disabled),
|
|
.data_disabled_len = data_len,
|
|
.data_enabled = std::move(data_enabled),
|
|
.data_enabled_len = data_len,
|
|
.data_offset = data_offset,
|
|
.data_offset_ptr = data_offset_ptr,
|
|
};
|
|
}
|
|
|
|
std::vector<uint8_t>* find_in_dll_map(
|
|
const std::string& dll_name, size_t offset, size_t size) {
|
|
|
|
auto dlls = DLL_MAP.find(dll_name);
|
|
if (dlls == DLL_MAP.end()) {
|
|
// not found; load DLL into map
|
|
DLL_MAP[dll_name] =
|
|
std::unique_ptr<std::vector<uint8_t>>(fileutils::bin_read(MODULE_PATH / dll_name));
|
|
}
|
|
|
|
// find file
|
|
auto file = DLL_MAP[dll_name].get();
|
|
|
|
// check bounds
|
|
if (file->size() < offset + size) {
|
|
return nullptr;
|
|
}
|
|
|
|
return file;
|
|
}
|
|
|
|
std::vector<uint8_t>* find_in_dll_map_org(
|
|
const std::string& dll_name, size_t offset, size_t size) {
|
|
|
|
auto dlls = DLL_MAP_ORG.find(dll_name);
|
|
if (dlls == DLL_MAP_ORG.end()) {
|
|
// not found; load DLL into map
|
|
DLL_MAP_ORG[dll_name] =
|
|
std::unique_ptr<std::vector<uint8_t>>(fileutils::bin_read(MODULE_PATH / dll_name));
|
|
}
|
|
|
|
// find file
|
|
auto file = DLL_MAP_ORG[dll_name].get();
|
|
if (!file) {
|
|
log_warning("patchmanager", "could not load file into memory: {}", dll_name);
|
|
return nullptr;
|
|
}
|
|
|
|
// check bounds
|
|
if (file->size() < offset + size) {
|
|
return nullptr;
|
|
}
|
|
|
|
return file;
|
|
}
|
|
|
|
void create_dll_backup(
|
|
std::vector<std::string>& written_list, const std::filesystem::path& dll_path) {
|
|
|
|
// if dll_path is not in written_list, create a file backup.
|
|
if (std::find(written_list.begin(), written_list.end(), dll_path.string()) == written_list.end()) {
|
|
written_list.push_back(dll_path.string());
|
|
auto dll_bak_path = std::filesystem::path(dll_path.string() + ".bak");
|
|
try {
|
|
if (!fileutils::file_exists(dll_bak_path)) {
|
|
std::filesystem::copy(dll_path, dll_bak_path);
|
|
}
|
|
log_info("patchmanager", "created DLL backup for: {}", dll_path.string());
|
|
} catch (const std::filesystem::filesystem_error& e) {
|
|
log_warning(
|
|
"patchmanager",
|
|
"filesystem error while creating DLL backup for {}, error: {}",
|
|
dll_path.string(), e.what());
|
|
}
|
|
}
|
|
}
|
|
|
|
std::string fix_up_dll_name(const std::string& dll_name) {
|
|
// IIDX omnimix dll name fix
|
|
if (dll_name == "bm2dx.dll" && avs::game::is_model("LDJ") && avs::game::REV[0] == 'X') {
|
|
return 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")) {
|
|
return avs::game::DLL_NAME;
|
|
}
|
|
|
|
return dll_name;
|
|
}
|
|
|
|
uint8_t* get_dll_offset_for_patch_apply(
|
|
const std::string& dll_name, const uint64_t data_offset, const size_t size_in_bytes) {
|
|
|
|
/// check if file exists
|
|
auto dll_path = MODULE_PATH / dll_name;
|
|
if (!fileutils::file_exists(dll_path)) {
|
|
log_warning("patchmanager", "{} does not exist", dll_path.string());
|
|
return nullptr;
|
|
}
|
|
|
|
// get module
|
|
auto module = libutils::try_module(dll_path);
|
|
if (!module) {
|
|
log_warning("patchmanager", "cannot get module: {}", dll_path.string());
|
|
return nullptr;
|
|
}
|
|
|
|
// convert offset to RVA
|
|
auto offset = libutils::offset2rva(dll_path, (intptr_t)data_offset);
|
|
if (offset == -1) {
|
|
log_warning(
|
|
"patchmanager", "cannot convert offset to RVA: {}, {}",
|
|
dll_path.string(), data_offset);
|
|
return nullptr;
|
|
}
|
|
|
|
// get module information
|
|
MODULEINFO module_info{};
|
|
if (!GetModuleInformation(
|
|
GetCurrentProcess(),
|
|
module,
|
|
&module_info,
|
|
sizeof(MODULEINFO))) {
|
|
|
|
log_warning(
|
|
"patchmanager", "GetModuleInformation failed for {}, gle: {}",
|
|
dll_path.string(), GetLastError());
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
// transmute pointer
|
|
auto dll_base = reinterpret_cast<uint8_t *>(module_info.lpBaseOfDll);
|
|
auto dll_image_size = static_cast<uintptr_t>(module_info.SizeOfImage);
|
|
|
|
// check bounds
|
|
auto max_offset = static_cast<uintptr_t>(offset + size_in_bytes);
|
|
if (max_offset >= dll_image_size) {
|
|
log_warning(
|
|
"patchmanager", "invalid offset bounds for {} ({})",
|
|
dll_name, max_offset);
|
|
return nullptr;
|
|
}
|
|
|
|
return &dll_base[offset];
|
|
}
|
|
|
|
int64_t parse_little_endian_int(uint8_t* bytes, size_t size) {
|
|
uint64_t result = 0;
|
|
for (size_t i = 0; i < size; i++) {
|
|
result |= bytes[i] << i * 8;
|
|
}
|
|
return static_cast<int64_t>(result);
|
|
}
|
|
|
|
void int_to_little_endian_bytes(int64_t value, uint8_t* bytes, size_t size) {
|
|
uint64_t v = static_cast<uint64_t>(value);
|
|
for (size_t i = 0; i < size; i++) {
|
|
bytes[i] = (v >> (i * 8)) & 0xff;
|
|
}
|
|
}
|
|
|
|
bool restore_bytes_from_dll_map_org(
|
|
uint8_t* destination, const std::string& dll_name, size_t offset, size_t size) {
|
|
|
|
const auto& orig_file = find_in_dll_map_org(dll_name, offset, size);
|
|
if (!orig_file) {
|
|
return false;
|
|
}
|
|
|
|
memcpy(destination, orig_file->data() + offset, size);
|
|
return true;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
void print_auto_apply_status(PatchData &patch) {
|
|
switch (patch.type) {
|
|
case PatchType::Union:
|
|
log_info(
|
|
"patchmanager", "auto apply: {} = {}",
|
|
patch.name, patch.selected_union_name);
|
|
break;
|
|
case PatchType::Integer:
|
|
log_info(
|
|
"patchmanager", "auto apply: {} = {}",
|
|
patch.name, patch.patch_number.value);
|
|
break;
|
|
case PatchType::Memory:
|
|
case PatchType::Signature:
|
|
default:
|
|
log_info("patchmanager", "auto apply: {} = ON", patch.name);
|
|
break;
|
|
}
|
|
}
|
|
}
|