mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-01 22:30:42 -07:00
patcher: refactor (#808)
## Link to GitHub Issue or related Pull Request, if one exists n/a ## Description of change Refactor patch manager. This one file had the UI logic, config save/load, parsing patches JSON, applying memory patches, everything all in one cpp file. Refactor and separate out the layers. ## Testing Sanity checked patch workflows.
This commit is contained in:
@@ -615,6 +615,12 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
overlay/windows/popn_sub.cpp
|
overlay/windows/popn_sub.cpp
|
||||||
overlay/windows/wnd_manager.cpp
|
overlay/windows/wnd_manager.cpp
|
||||||
|
|
||||||
|
# patcher
|
||||||
|
patcher/config.cpp
|
||||||
|
patcher/lifecycle.cpp
|
||||||
|
patcher/loader.cpp
|
||||||
|
patcher/runtime.cpp
|
||||||
|
|
||||||
# external
|
# external
|
||||||
external/easywsclient/easywsclient.cpp
|
external/easywsclient/easywsclient.cpp
|
||||||
|
|
||||||
|
|||||||
@@ -102,7 +102,7 @@
|
|||||||
#include "misc/wintouchemu.h"
|
#include "misc/wintouchemu.h"
|
||||||
#include "overlay/overlay.h"
|
#include "overlay/overlay.h"
|
||||||
#include "overlay/notifications.h"
|
#include "overlay/notifications.h"
|
||||||
#include "overlay/windows/patch_manager.h"
|
#include "patcher/patch_manager.h"
|
||||||
#include "overlay/windows/iidx_seg.h"
|
#include "overlay/windows/iidx_seg.h"
|
||||||
#include "overlay/windows/obs.h"
|
#include "overlay/windows/obs.h"
|
||||||
#include "rawinput/rawinput.h"
|
#include "rawinput/rawinput.h"
|
||||||
@@ -889,7 +889,7 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
options[launcher::Options::ScreenResizeConfigPath].value_text();
|
options[launcher::Options::ScreenResizeConfigPath].value_text();
|
||||||
}
|
}
|
||||||
if (options[launcher::Options::PatchManagerConfigPath].is_active()) {
|
if (options[launcher::Options::PatchManagerConfigPath].is_active()) {
|
||||||
overlay::windows::PATCH_MANAGER_CFG_PATH_OVERRIDE =
|
patcher::PATCH_MANAGER_CFG_PATH_OVERRIDE =
|
||||||
options[launcher::Options::PatchManagerConfigPath].value_text();
|
options[launcher::Options::PatchManagerConfigPath].value_text();
|
||||||
}
|
}
|
||||||
if (options[launcher::Options::PathToAppConfig].is_active()) {
|
if (options[launcher::Options::PathToAppConfig].is_active()) {
|
||||||
@@ -2517,10 +2517,8 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
// copy defaults to nvram
|
// copy defaults to nvram
|
||||||
avs::core::copy_defaults();
|
avs::core::copy_defaults();
|
||||||
|
|
||||||
// prepare patches
|
// prepare patches (registers the DLL-load notification before the game DLL loads)
|
||||||
{
|
patcher::init();
|
||||||
overlay::windows::PatchManager patch_manager(nullptr, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// load game
|
// load game
|
||||||
avs::game::load_dll();
|
avs::game::load_dll();
|
||||||
@@ -2648,9 +2646,7 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// apply patches
|
// apply patches
|
||||||
{
|
patcher::apply_patches_on_start();
|
||||||
overlay::windows::PatchManager patch_manager(nullptr, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// load AVS-EA3
|
// load AVS-EA3
|
||||||
avs::ea3::boot(easrv_port, easrv_maint, easrv_smart);
|
avs::ea3::boot(easrv_port, easrv_maint, easrv_smart);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,183 +1,21 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "overlay/window.h"
|
#include "overlay/window.h"
|
||||||
#include <map>
|
#include "patcher/patch_manager.h"
|
||||||
#include <functional>
|
|
||||||
#include <filesystem>
|
|
||||||
#include <optional>
|
|
||||||
#include "external/rapidjson/document.h"
|
|
||||||
|
|
||||||
namespace overlay::windows {
|
namespace overlay::windows {
|
||||||
|
|
||||||
enum class PatchType {
|
|
||||||
Unknown,
|
|
||||||
Memory,
|
|
||||||
Signature,
|
|
||||||
Union,
|
|
||||||
Integer,
|
|
||||||
};
|
|
||||||
|
|
||||||
enum class PatchStatus {
|
|
||||||
Error,
|
|
||||||
Disabled,
|
|
||||||
Enabled,
|
|
||||||
};
|
|
||||||
|
|
||||||
enum class PatchUrlStatus {
|
|
||||||
Valid,
|
|
||||||
Invalid,
|
|
||||||
Unapplied,
|
|
||||||
ValidButNoData,
|
|
||||||
Partial,
|
|
||||||
};
|
|
||||||
|
|
||||||
struct MemoryPatch {
|
|
||||||
std::string dll_name = "";
|
|
||||||
std::shared_ptr<uint8_t[]> data_disabled = nullptr;
|
|
||||||
size_t data_disabled_len = 0;
|
|
||||||
std::shared_ptr<uint8_t[]> data_enabled = nullptr;
|
|
||||||
size_t data_enabled_len = 0;
|
|
||||||
uint64_t data_offset = 0;
|
|
||||||
uint8_t *data_offset_ptr = nullptr;
|
|
||||||
bool fatal_error = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct PatchData;
|
|
||||||
struct SignaturePatch {
|
|
||||||
std::string dll_name = "";
|
|
||||||
std::string signature = "", replacement = "";
|
|
||||||
uint64_t offset = 0;
|
|
||||||
int64_t usage = 0;
|
|
||||||
|
|
||||||
MemoryPatch to_memory(PatchData *patch);
|
|
||||||
};
|
|
||||||
|
|
||||||
struct UnionPatch {
|
|
||||||
std::string name = "";
|
|
||||||
std::string dll_name = "";
|
|
||||||
std::shared_ptr<uint8_t[]> data = nullptr;
|
|
||||||
size_t data_len = 0;
|
|
||||||
uint64_t offset = 0;
|
|
||||||
uint8_t* data_offset_ptr = nullptr;
|
|
||||||
bool fatal_error = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct NumberPatch {
|
|
||||||
std::string dll_name = "";
|
|
||||||
uint64_t data_offset = 0;
|
|
||||||
uint8_t* data_offset_ptr = nullptr;
|
|
||||||
int32_t min;
|
|
||||||
int32_t max;
|
|
||||||
int32_t value;
|
|
||||||
size_t size_in_bytes;
|
|
||||||
bool fatal_error = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct PatchData {
|
|
||||||
bool enabled;
|
|
||||||
std::string game_code;
|
|
||||||
int datecode_min = 0;
|
|
||||||
int datecode_max = 0;
|
|
||||||
std::string name, description, caution;
|
|
||||||
std::string name_in_lower_case = "";
|
|
||||||
PatchType type;
|
|
||||||
bool preset;
|
|
||||||
std::vector<MemoryPatch> patches_memory;
|
|
||||||
std::vector<UnionPatch> patches_union;
|
|
||||||
NumberPatch patch_number;
|
|
||||||
PatchStatus last_status;
|
|
||||||
std::string hash;
|
|
||||||
bool unverified = false;
|
|
||||||
std::string peIdentifier;
|
|
||||||
std::string error_reason = "";
|
|
||||||
|
|
||||||
// for union patch only
|
|
||||||
std::string selected_union_name = "";
|
|
||||||
};
|
|
||||||
|
|
||||||
extern std::optional<std::string> PATCH_MANAGER_CFG_PATH_OVERRIDE;
|
|
||||||
|
|
||||||
std::string get_game_identifier(const std::filesystem::path& dll_path, bool print_info=false);
|
|
||||||
|
|
||||||
class PatchManager : public Window {
|
class PatchManager : public Window {
|
||||||
public:
|
public:
|
||||||
|
explicit PatchManager(SpiceOverlay *overlay);
|
||||||
PatchManager(SpiceOverlay *overlay, bool apply_patches = false);
|
|
||||||
~PatchManager() override;
|
~PatchManager() override;
|
||||||
|
|
||||||
void build_content() override;
|
void build_content() override;
|
||||||
void reload_local_patches(bool apply_patches = false);
|
|
||||||
bool import_remote_patches_to_disk();
|
|
||||||
bool load_from_patches_json(bool apply_patches);
|
|
||||||
bool import_remote_patches_for_dll(const std::string& url, const std::string& dll_name);
|
|
||||||
void hard_apply_patches();
|
|
||||||
void load_embedded_patches(bool apply_patches);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
// configuration
|
|
||||||
static std::filesystem::path config_path;
|
|
||||||
static bool config_dirty;
|
|
||||||
static bool setting_auto_apply;
|
|
||||||
static std::vector<std::string> setting_auto_apply_list;
|
|
||||||
static std::vector<std::string> setting_patches_enabled;
|
|
||||||
static std::map<std::string, std::string> setting_union_patches_enabled;
|
|
||||||
static std::map<std::string, int64_t> setting_int_patches_enabled;
|
|
||||||
static std::string patch_url;
|
|
||||||
static std::string patch_name_filter;
|
static std::string patch_name_filter;
|
||||||
|
|
||||||
static std::filesystem::path LOCAL_PATCHES_PATH;
|
|
||||||
static std::string ACTIVE_JSON_FILE;
|
|
||||||
|
|
||||||
// patches
|
|
||||||
static std::vector<PatchData> patches;
|
|
||||||
static bool local_patches_initialized;
|
|
||||||
|
|
||||||
// cached sorted view of `patches` for the table, stored as indices so a
|
|
||||||
// stale entry can never dangle if `patches` is rebuilt; rebuilt only when
|
|
||||||
// the sort order changes or the patch list is reloaded (not every frame)
|
|
||||||
static std::vector<size_t> patches_sorted;
|
|
||||||
|
|
||||||
void config_load();
|
|
||||||
void config_save();
|
|
||||||
|
|
||||||
// rebuild the cached sorted view of `patches` from the active table's
|
|
||||||
// sort specs; no-op unless the sort changed or the patch list changed
|
|
||||||
void update_sorted_patches();
|
void update_sorted_patches();
|
||||||
|
void show_patch_tooltip(const patcher::PatchData& patch);
|
||||||
void append_patches(
|
|
||||||
std::string &patches_json,
|
|
||||||
bool apply_patches = false,
|
|
||||||
std::function<bool(const PatchData&)> filter = std::function<bool(const PatchData&)>(),
|
|
||||||
std::string pe_identifier_for_patch = "");
|
|
||||||
|
|
||||||
void show_patch_tooltip(const PatchData& patch);
|
|
||||||
|
|
||||||
bool is_game_id_wildcard_matched(const std::string& id_from_config);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
PatchStatus is_patch_active(PatchData &patch);
|
|
||||||
bool apply_patch(PatchData &patch, bool active);
|
|
||||||
|
|
||||||
int64_t parse_little_endian_int(uint8_t* bytes, size_t size);
|
|
||||||
void int_to_little_endian_bytes(int64_t value, uint8_t* bytes, size_t size);
|
|
||||||
|
|
||||||
std::vector<uint8_t>* find_in_dll_map(
|
|
||||||
const std::string& dll_name, size_t offset, size_t size);
|
|
||||||
std::vector<uint8_t>* find_in_dll_map_org(
|
|
||||||
const std::string& dll_name, size_t offset, size_t size);
|
|
||||||
|
|
||||||
bool restore_bytes_from_dll_map_org(
|
|
||||||
uint8_t* destination, const std::string& dll_name, size_t offset, size_t size);
|
|
||||||
|
|
||||||
void create_dll_backup(
|
|
||||||
std::vector<std::filesystem::path>& written_list, const std::filesystem::path& dll_path);
|
|
||||||
std::string fix_up_dll_name(const std::string& 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);
|
|
||||||
|
|
||||||
uint64_t parse_json_data_offset(
|
|
||||||
const std::string &patch_name, const rapidjson::Value &value);
|
|
||||||
|
|
||||||
void print_auto_apply_status(PatchData &patch);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,265 @@
|
|||||||
|
#include "internal.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include "avs/game.h"
|
||||||
|
#include "cfg/configurator.h"
|
||||||
|
#include "external/hash-library/sha256.h"
|
||||||
|
#include "external/rapidjson/document.h"
|
||||||
|
#include "external/rapidjson/prettywriter.h"
|
||||||
|
#include "external/rapidjson/stringbuffer.h"
|
||||||
|
#include "util/fileutils.h"
|
||||||
|
#include "util/logging.h"
|
||||||
|
|
||||||
|
using namespace rapidjson;
|
||||||
|
|
||||||
|
namespace patcher {
|
||||||
|
|
||||||
|
bool is_game_id_wildcard_matched(const std::string& id_from_config) {
|
||||||
|
return ((id_from_config.compare(0, 3, avs::game::MODEL) == 0) &&
|
||||||
|
(id_from_config.compare(10, 10, avs::game::EXT) == 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
void 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: {}", static_cast<uint32_t>(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
|
||||||
|
const 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
|
||||||
|
const std::string entry_id = entry.GetString();
|
||||||
|
|
||||||
|
if (!setting_auto_apply) {
|
||||||
|
if (game_id == entry_id) {
|
||||||
|
// exact match
|
||||||
|
setting_auto_apply = true;
|
||||||
|
log_misc(
|
||||||
|
"patchmanager",
|
||||||
|
"matched auto apply entry by full game identifier: {}",
|
||||||
|
entry_id);
|
||||||
|
|
||||||
|
} else if (is_game_id_wildcard_matched(entry_id)) {
|
||||||
|
// match on model and ext, ignoring dest/spec/rev
|
||||||
|
// sample: LDJ:J:E:A:2025011400
|
||||||
|
setting_auto_apply = true;
|
||||||
|
log_misc(
|
||||||
|
"patchmanager",
|
||||||
|
"matched auto apply entry by partial game identifier: {}:?:?:?:{}",
|
||||||
|
avs::game::MODEL, avs::game::EXT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// move to list
|
||||||
|
setting_auto_apply_list.emplace_back(entry_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!setting_auto_apply) {
|
||||||
|
log_misc(
|
||||||
|
"patchmanager",
|
||||||
|
"no auto apply entry matched, patches will not load automatically");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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: {}", static_cast<uint32_t>(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 || is_game_id_wildcard_matched(entry)) {
|
||||||
|
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::write_config_file("patchmanager", config_path, buffer.GetString())) {
|
||||||
|
config_dirty = false;
|
||||||
|
} else {
|
||||||
|
log_warning("patchmanager", "unable to save config file");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "patch_manager.h"
|
||||||
|
#include "external/rapidjson/document.h"
|
||||||
|
#include "util/nt_loader.h"
|
||||||
|
|
||||||
|
namespace patcher {
|
||||||
|
|
||||||
|
// internal engine state (patcher module only)
|
||||||
|
extern std::vector<std::string> setting_auto_apply_list;
|
||||||
|
extern std::vector<std::string> setting_patches_enabled;
|
||||||
|
extern std::map<std::string, std::string> setting_union_patches_enabled;
|
||||||
|
extern std::map<std::string, int64_t> setting_int_patches_enabled;
|
||||||
|
extern std::filesystem::path LOCAL_PATCHES_PATH;
|
||||||
|
extern std::map<std::string, std::vector<std::string>> EXTRA_DLLS;
|
||||||
|
extern bool ldr_registered;
|
||||||
|
extern void *ldr_notify_cookie;
|
||||||
|
extern std::vector<std::string> ldr_target_libraries;
|
||||||
|
|
||||||
|
// internal helpers
|
||||||
|
void append_patches(
|
||||||
|
std::string& patches_json,
|
||||||
|
bool apply_patches = false,
|
||||||
|
std::function<bool(const PatchData&)> filter = std::function<bool(const PatchData&)>(),
|
||||||
|
std::string pe_identifier_for_patch = "");
|
||||||
|
bool is_game_id_wildcard_matched(const std::string& id_from_config);
|
||||||
|
std::string getFromUrl(const std::string& dll_name, const std::string& url);
|
||||||
|
bool load_from_patches_json(bool apply_patches);
|
||||||
|
void load_embedded_patches(bool apply_patches);
|
||||||
|
bool import_remote_patches_for_dll(const std::string& url, const std::string& dll_name);
|
||||||
|
VOID CALLBACK loader_notification(ULONG reason, PCLDR_DLL_NOTIFICATION_DATA data, PVOID context);
|
||||||
|
|
||||||
|
std::string patch_hash(PatchData& patch);
|
||||||
|
void clear_dll_maps();
|
||||||
|
|
||||||
|
int64_t parse_little_endian_int(uint8_t *bytes, size_t size);
|
||||||
|
void int_to_little_endian_bytes(int64_t value, uint8_t *bytes, size_t size);
|
||||||
|
|
||||||
|
std::vector<uint8_t> *find_in_dll_map(
|
||||||
|
const std::string& dll_name, size_t offset, size_t size);
|
||||||
|
std::vector<uint8_t> *find_in_dll_map_org(
|
||||||
|
const std::string& dll_name, size_t offset, size_t size);
|
||||||
|
|
||||||
|
bool restore_bytes_from_dll_map_org(
|
||||||
|
uint8_t *destination, const std::string& dll_name, size_t offset, size_t size);
|
||||||
|
|
||||||
|
void create_dll_backup(
|
||||||
|
std::vector<std::filesystem::path>& written_list, const std::filesystem::path& dll_path);
|
||||||
|
std::string fix_up_dll_name(const std::string& dll_name);
|
||||||
|
uint8_t *get_dll_offset_for_patch_apply(
|
||||||
|
const std::string& dll_name, uint64_t data_offset, size_t size_in_bytes);
|
||||||
|
|
||||||
|
uint64_t parse_json_data_offset(
|
||||||
|
const std::string& patch_name, const rapidjson::Value& value);
|
||||||
|
|
||||||
|
void print_auto_apply_status(PatchData& patch);
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
#include "internal.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include "avs/game.h"
|
||||||
|
#include "cfg/configurator.h"
|
||||||
|
#include "util/fileutils.h"
|
||||||
|
#include "util/logging.h"
|
||||||
|
#include "util/utils.h"
|
||||||
|
|
||||||
|
namespace patcher {
|
||||||
|
|
||||||
|
std::optional<std::string> PATCH_MANAGER_CFG_PATH_OVERRIDE;
|
||||||
|
|
||||||
|
std::filesystem::path config_path;
|
||||||
|
bool config_dirty = false;
|
||||||
|
bool setting_auto_apply = false;
|
||||||
|
|
||||||
|
std::vector<std::string> setting_auto_apply_list;
|
||||||
|
std::vector<std::string> setting_patches_enabled;
|
||||||
|
|
||||||
|
std::map<std::string, std::string> setting_union_patches_enabled;
|
||||||
|
std::map<std::string, int64_t> setting_int_patches_enabled;
|
||||||
|
|
||||||
|
std::string patch_url;
|
||||||
|
|
||||||
|
std::filesystem::path LOCAL_PATCHES_PATH("patches");
|
||||||
|
std::string ACTIVE_JSON_FILE;
|
||||||
|
|
||||||
|
std::vector<PatchData> patches;
|
||||||
|
bool local_patches_initialized = false;
|
||||||
|
std::vector<size_t> patches_sorted;
|
||||||
|
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", "libshare-pj.dll", "boot.dll"}}
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string url_fetch_errors;
|
||||||
|
size_t url_recent_idx = -1;
|
||||||
|
std::vector<std::string> url_recents;
|
||||||
|
|
||||||
|
bool ldr_registered = false;
|
||||||
|
void *ldr_notify_cookie = nullptr;
|
||||||
|
std::vector<std::string> ldr_target_libraries;
|
||||||
|
|
||||||
|
void init() {
|
||||||
|
if (PATCH_MANAGER_CFG_PATH_OVERRIDE.has_value()) {
|
||||||
|
config_path = PATCH_MANAGER_CFG_PATH_OVERRIDE.value();
|
||||||
|
log_info("patchmanager", "using custom config file path: {}", config_path);
|
||||||
|
} else {
|
||||||
|
config_path =
|
||||||
|
fileutils::get_config_file_path("patchmanager", "spicetools_patch_manager.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
// register for DLL load notifications so patches can be (re)applied as
|
||||||
|
// the game's target libraries come into memory. registration happens
|
||||||
|
// once and is left in place for the lifetime of the process.
|
||||||
|
if (!ldr_registered) {
|
||||||
|
ldr_target_libraries = getExtraDlls(avs::game::DLL_NAME);
|
||||||
|
ldr_target_libraries.push_back(avs::game::DLL_NAME);
|
||||||
|
|
||||||
|
const auto register_fn = reinterpret_cast<decltype(&LdrRegisterDllNotification)>(
|
||||||
|
GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "LdrRegisterDllNotification"));
|
||||||
|
|
||||||
|
if (register_fn && NT_SUCCESS(register_fn(
|
||||||
|
0, &loader_notification, nullptr, &ldr_notify_cookie))) {
|
||||||
|
log_info("patchmanager", "registered for DLL load notifications");
|
||||||
|
} else {
|
||||||
|
log_warning("patchmanager", "failed to register for DLL load notifications");
|
||||||
|
}
|
||||||
|
|
||||||
|
ldr_registered = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!local_patches_initialized) {
|
||||||
|
patch_url.clear();
|
||||||
|
if (fileutils::file_exists(config_path)) {
|
||||||
|
config_load();
|
||||||
|
}
|
||||||
|
if (cfg::CONFIGURATOR_STANDALONE) {
|
||||||
|
reload_local_patches(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void apply_patches_on_start() {
|
||||||
|
if (!local_patches_initialized) {
|
||||||
|
reload_local_patches(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VOID CALLBACK loader_notification(
|
||||||
|
ULONG reason,
|
||||||
|
PCLDR_DLL_NOTIFICATION_DATA data,
|
||||||
|
PVOID) {
|
||||||
|
|
||||||
|
if (reason == LDR_DLL_NOTIFICATION_REASON_LOADED) {
|
||||||
|
const auto dll = strtolower(std::filesystem::path({
|
||||||
|
data->Loaded.FullDllName->Buffer,
|
||||||
|
data->Loaded.FullDllName->Length / sizeof(wchar_t)
|
||||||
|
}).filename().string());
|
||||||
|
|
||||||
|
if (std::ranges::find(ldr_target_libraries, dll) != ldr_target_libraries.end()) {
|
||||||
|
reload_local_patches(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,134 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <functional>
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace patcher {
|
||||||
|
|
||||||
|
enum class PatchType {
|
||||||
|
Unknown,
|
||||||
|
Memory,
|
||||||
|
Signature,
|
||||||
|
Union,
|
||||||
|
Integer,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class PatchStatus {
|
||||||
|
Error,
|
||||||
|
Disabled,
|
||||||
|
Enabled,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class PatchUrlStatus {
|
||||||
|
Valid,
|
||||||
|
Invalid,
|
||||||
|
Unapplied,
|
||||||
|
ValidButNoData,
|
||||||
|
Partial,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MemoryPatch {
|
||||||
|
std::string dll_name = "";
|
||||||
|
std::shared_ptr<uint8_t[]> data_disabled = nullptr;
|
||||||
|
size_t data_disabled_len = 0;
|
||||||
|
std::shared_ptr<uint8_t[]> data_enabled = nullptr;
|
||||||
|
size_t data_enabled_len = 0;
|
||||||
|
uint64_t data_offset = 0;
|
||||||
|
uint8_t *data_offset_ptr = nullptr;
|
||||||
|
bool fatal_error = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PatchData;
|
||||||
|
struct SignaturePatch {
|
||||||
|
std::string dll_name = "";
|
||||||
|
std::string signature = "", replacement = "";
|
||||||
|
uint64_t offset = 0;
|
||||||
|
int64_t usage = 0;
|
||||||
|
|
||||||
|
MemoryPatch to_memory(PatchData *patch);
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UnionPatch {
|
||||||
|
std::string name = "";
|
||||||
|
std::string dll_name = "";
|
||||||
|
std::shared_ptr<uint8_t[]> data = nullptr;
|
||||||
|
size_t data_len = 0;
|
||||||
|
uint64_t offset = 0;
|
||||||
|
uint8_t *data_offset_ptr = nullptr;
|
||||||
|
bool fatal_error = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct NumberPatch {
|
||||||
|
std::string dll_name = "";
|
||||||
|
uint64_t data_offset = 0;
|
||||||
|
uint8_t *data_offset_ptr = nullptr;
|
||||||
|
int32_t min;
|
||||||
|
int32_t max;
|
||||||
|
int32_t value;
|
||||||
|
size_t size_in_bytes;
|
||||||
|
bool fatal_error = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PatchData {
|
||||||
|
bool enabled;
|
||||||
|
std::string game_code;
|
||||||
|
int datecode_min = 0;
|
||||||
|
int datecode_max = 0;
|
||||||
|
std::string name, description, caution;
|
||||||
|
std::string name_in_lower_case = "";
|
||||||
|
PatchType type;
|
||||||
|
bool preset;
|
||||||
|
std::vector<MemoryPatch> patches_memory;
|
||||||
|
std::vector<UnionPatch> patches_union;
|
||||||
|
NumberPatch patch_number;
|
||||||
|
PatchStatus last_status;
|
||||||
|
std::string hash;
|
||||||
|
bool unverified = false;
|
||||||
|
std::string peIdentifier;
|
||||||
|
std::string error_reason = "";
|
||||||
|
|
||||||
|
// for union patch only
|
||||||
|
std::string selected_union_name = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
extern std::optional<std::string> PATCH_MANAGER_CFG_PATH_OVERRIDE;
|
||||||
|
|
||||||
|
std::string get_game_identifier(const std::filesystem::path& dll_path, bool print_info = false);
|
||||||
|
|
||||||
|
// lifecycle: init() sets up the config path and registers for DLL-load
|
||||||
|
// notifications; apply_patches_on_start() applies auto-apply patches once the
|
||||||
|
// game DLL is loaded. both are safe to call repeatedly.
|
||||||
|
void init();
|
||||||
|
void apply_patches_on_start();
|
||||||
|
|
||||||
|
// patch load / apply operations (also driven by the overlay window)
|
||||||
|
void reload_local_patches(bool apply_patches = false);
|
||||||
|
bool import_remote_patches_to_disk();
|
||||||
|
void hard_apply_patches();
|
||||||
|
void config_load();
|
||||||
|
void config_save();
|
||||||
|
std::vector<std::string> getExtraDlls(const std::string& firstDll);
|
||||||
|
|
||||||
|
// process-wide patch engine state, rendered and edited by the overlay window
|
||||||
|
extern std::filesystem::path config_path;
|
||||||
|
extern bool config_dirty;
|
||||||
|
extern bool setting_auto_apply;
|
||||||
|
extern std::string patch_url;
|
||||||
|
extern std::string ACTIVE_JSON_FILE;
|
||||||
|
extern std::vector<PatchData> patches;
|
||||||
|
extern bool local_patches_initialized;
|
||||||
|
extern std::vector<size_t> patches_sorted;
|
||||||
|
extern std::string url_fetch_errors;
|
||||||
|
extern size_t url_recent_idx;
|
||||||
|
extern std::vector<std::string> url_recents;
|
||||||
|
|
||||||
|
PatchStatus is_patch_active(PatchData& patch);
|
||||||
|
bool apply_patch(PatchData& patch, bool active);
|
||||||
|
|
||||||
|
std::string displayPath(const std::filesystem::path& path);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user