patcher: patch groups (#812)
## Link to GitHub Issue or related Pull Request, if one exists Fixes #245 ## Description of change Introduce patch groups. JSON schema now allows for a `"type": "group"`: ```json { "type": "group", "gameCode": "LDJ", "id": "timer-freeze", "name": "Timer Freeze Patches", "description": "Freezes various timers in the game." }, { "name": "Standard/Menu Timer Freeze", "description": "Freezes all non-premium area timers.", "caution": "", "gameCode": "LDJ", "type": "memory", "group": "timer-freeze", "patches": [ { "offset": 9962743, "dllName": "bm2dx.dll", "dataDisabled": "0F84", "dataEnabled": "90E9" } ] }, { "name": "Premium Free Timer Freeze", "description": "Freezes all premium area timers.", "caution": "", "gameCode": "LDJ", "type": "memory", "group": "timer-freeze", "patches": [ { "offset": 9111965, "dllName": "bm2dx.dll", "dataDisabled": "7E", "dataEnabled": "EB" } ] }, ``` In the UI, these will show up as tree nodes. The parent is not actually treated as a patch; e.g., its state is not saved to the patch manager config file; only the child patches states are managed, the parent's state only bubble up only in the UI. In downlevel versions of spice, group parents will show up as a patch of invalid type. Child patches will still show up as individual patches. ## Testing *how was the code tested?*
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <utility>
|
||||
|
||||
#include "patch_manager.h"
|
||||
#include "external/rapidjson/document.h"
|
||||
#include "util/nt_loader.h"
|
||||
@@ -11,6 +14,7 @@ namespace patcher {
|
||||
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::map<std::pair<std::string, std::string>, PatchGroup> patch_groups;
|
||||
extern std::filesystem::path LOCAL_PATCHES_PATH;
|
||||
extern std::map<std::string, std::vector<std::string>> EXTRA_DLLS;
|
||||
extern bool ldr_registered;
|
||||
@@ -28,6 +32,17 @@ namespace patcher {
|
||||
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);
|
||||
bool is_patch_group_definition(const rapidjson::Value& patch);
|
||||
std::map<std::pair<std::string, std::string>, PatchGroup> parse_patch_group_definitions(
|
||||
const rapidjson::Document& doc);
|
||||
std::string resolve_patch_group_id(
|
||||
const rapidjson::Value& patch,
|
||||
const std::map<std::pair<std::string, std::string>, PatchGroup>& groups,
|
||||
const std::string& game_code,
|
||||
const char *patch_name);
|
||||
void register_patch_group(
|
||||
PatchData& patch,
|
||||
const std::map<std::pair<std::string, std::string>, PatchGroup>& definitions);
|
||||
VOID CALLBACK loader_notification(ULONG reason, PCLDR_DLL_NOTIFICATION_DATA data, PVOID context);
|
||||
|
||||
std::string patch_hash(PatchData& patch);
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace patcher {
|
||||
std::vector<PatchData> patches;
|
||||
bool local_patches_initialized = false;
|
||||
std::vector<size_t> patches_sorted;
|
||||
std::map<std::pair<std::string, std::string>, PatchGroup> patch_groups;
|
||||
std::map<std::string, std::vector<std::string>> EXTRA_DLLS = {
|
||||
{"jubeat.dll", {"music_db.dll", "coin.dll"}},
|
||||
{"arkmdxp3.dll", {"gamemdx.dll"}},
|
||||
|
||||
@@ -199,8 +199,13 @@ namespace patcher {
|
||||
log_warning("patchmanager", "embedded patches json file parse error: {}", static_cast<uint32_t>(error));
|
||||
}
|
||||
|
||||
const auto group_definitions = parse_patch_group_definitions(doc);
|
||||
|
||||
// iterate patches
|
||||
for (auto &patch : doc.GetArray()) {
|
||||
if (is_patch_group_definition(patch)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// verfiy patch data
|
||||
auto name_it = patch.FindMember("name");
|
||||
@@ -214,6 +219,9 @@ namespace patcher {
|
||||
name_it->value.GetString());
|
||||
continue;
|
||||
}
|
||||
const std::string game_code(
|
||||
game_code_it->value.GetString(),
|
||||
game_code_it->value.GetStringLength());
|
||||
auto description_it = patch.FindMember("description");
|
||||
if (description_it == patch.MemberEnd() || !description_it->value.IsString()) {
|
||||
log_warning("patchmanager", "failed to parse description for {}",
|
||||
@@ -235,7 +243,7 @@ namespace patcher {
|
||||
// build patch data
|
||||
PatchData patch_data {
|
||||
.enabled = false,
|
||||
.game_code = game_code_it->value.GetString(),
|
||||
.game_code = game_code,
|
||||
.datecode_min = 0,
|
||||
.datecode_max = 0,
|
||||
.name = name_it->value.GetString(),
|
||||
@@ -247,6 +255,11 @@ namespace patcher {
|
||||
.patches_memory = std::vector<MemoryPatch>(),
|
||||
.patches_union = std::vector<UnionPatch>(),
|
||||
.patch_number = NumberPatch(),
|
||||
.group_id = resolve_patch_group_id(
|
||||
patch,
|
||||
group_definitions,
|
||||
game_code,
|
||||
name_it->value.GetString()),
|
||||
.last_status = PatchStatus::Disabled,
|
||||
.hash = "",
|
||||
.unverified = false,
|
||||
@@ -533,6 +546,7 @@ namespace patcher {
|
||||
}
|
||||
|
||||
// auto apply
|
||||
register_patch_group(patch_data, group_definitions);
|
||||
if (apply_patches && setting_auto_apply && patch_data.enabled) {
|
||||
print_auto_apply_status(patch_data);
|
||||
apply_patch(patch_data, true);
|
||||
@@ -663,6 +677,7 @@ namespace patcher {
|
||||
|
||||
// clear old patches
|
||||
patches.clear();
|
||||
patch_groups.clear();
|
||||
// drop the cached sorted view so the table rebuilds it (in default order)
|
||||
// on the next frame
|
||||
patches_sorted.clear();
|
||||
@@ -719,6 +734,7 @@ namespace patcher {
|
||||
bool imported = false;
|
||||
// clear old patches
|
||||
patches.clear();
|
||||
patch_groups.clear();
|
||||
patches_sorted.clear();
|
||||
url_fetch_errors.clear();
|
||||
|
||||
@@ -755,8 +771,13 @@ namespace patcher {
|
||||
rapidjson::GetParseError_En(error));
|
||||
}
|
||||
|
||||
const auto group_definitions = parse_patch_group_definitions(doc);
|
||||
|
||||
// iterate patches
|
||||
for (auto &patch : doc.GetArray()) {
|
||||
if (is_patch_group_definition(patch)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// verfiy patch data
|
||||
auto name_it = patch.FindMember("name");
|
||||
@@ -778,6 +799,9 @@ namespace patcher {
|
||||
name_it->value.GetString());
|
||||
continue;
|
||||
}
|
||||
const std::string game_code(
|
||||
game_code_it->value.GetString(),
|
||||
game_code_it->value.GetStringLength());
|
||||
auto description_it = patch.FindMember("description");
|
||||
if (description_it == patch.MemberEnd() || !description_it->value.IsString()) {
|
||||
log_warning("patchmanager", "failed to parse description for {}",
|
||||
@@ -809,7 +833,7 @@ namespace patcher {
|
||||
// build patch data
|
||||
PatchData patch_data {
|
||||
.enabled = false,
|
||||
.game_code = game_code_it->value.GetString(),
|
||||
.game_code = game_code,
|
||||
.datecode_min = 0,
|
||||
.datecode_max = 0,
|
||||
.name = name_it->value.GetString(),
|
||||
@@ -821,6 +845,11 @@ namespace patcher {
|
||||
.patches_memory = std::vector<MemoryPatch>(),
|
||||
.patches_union = std::vector<UnionPatch>(),
|
||||
.patch_number = NumberPatch(),
|
||||
.group_id = resolve_patch_group_id(
|
||||
patch,
|
||||
group_definitions,
|
||||
game_code,
|
||||
name_it->value.GetString()),
|
||||
.last_status = PatchStatus::Disabled,
|
||||
.hash = "",
|
||||
.unverified = false,
|
||||
@@ -1264,6 +1293,7 @@ namespace patcher {
|
||||
}
|
||||
|
||||
// auto apply
|
||||
register_patch_group(patch_data, group_definitions);
|
||||
if (apply_patches && setting_auto_apply && patch_data.enabled) {
|
||||
print_auto_apply_status(patch_data);
|
||||
apply_patch(patch_data, true);
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
#include "internal.h"
|
||||
|
||||
#include <cstring>
|
||||
#include "util/logging.h"
|
||||
#include "util/utils.h"
|
||||
|
||||
namespace patcher {
|
||||
|
||||
static std::pair<std::string, std::string> make_patch_group_key(
|
||||
const std::string& game_code,
|
||||
const std::string& group_id) {
|
||||
return {strtolower(game_code), group_id};
|
||||
}
|
||||
|
||||
static bool has_embedded_null(const rapidjson::Value& value) {
|
||||
return strlen(value.GetString()) != value.GetStringLength();
|
||||
}
|
||||
|
||||
bool is_patch_group_definition(const rapidjson::Value& patch) {
|
||||
if (!patch.IsObject()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto type_it = patch.FindMember("type");
|
||||
return type_it != patch.MemberEnd()
|
||||
&& type_it->value.IsString()
|
||||
&& type_it->value.GetStringLength() == strlen("group")
|
||||
&& !_stricmp(type_it->value.GetString(), "group");
|
||||
}
|
||||
|
||||
std::map<std::pair<std::string, std::string>, PatchGroup> parse_patch_group_definitions(
|
||||
const rapidjson::Document& doc) {
|
||||
std::map<std::pair<std::string, std::string>, PatchGroup> groups;
|
||||
|
||||
for (const auto& patch : doc.GetArray()) {
|
||||
if (!is_patch_group_definition(patch)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto id_it = patch.FindMember("id");
|
||||
const auto game_code_it = patch.FindMember("gameCode");
|
||||
const auto name_it = patch.FindMember("name");
|
||||
if (id_it == patch.MemberEnd() || !id_it->value.IsString()
|
||||
|| id_it->value.GetStringLength() == 0
|
||||
|| has_embedded_null(id_it->value)
|
||||
|| game_code_it == patch.MemberEnd() || !game_code_it->value.IsString()
|
||||
|| game_code_it->value.GetStringLength() == 0
|
||||
|| has_embedded_null(game_code_it->value)
|
||||
|| name_it == patch.MemberEnd() || !name_it->value.IsString()
|
||||
|| name_it->value.GetStringLength() == 0
|
||||
|| has_embedded_null(name_it->value)) {
|
||||
log_warning("patchmanager", "invalid patch group definition");
|
||||
continue;
|
||||
}
|
||||
|
||||
PatchGroup group;
|
||||
group.name.assign(name_it->value.GetString(), name_it->value.GetStringLength());
|
||||
group.name_in_lower_case = strtolower(group.name);
|
||||
|
||||
const std::string group_id(
|
||||
id_it->value.GetString(),
|
||||
id_it->value.GetStringLength());
|
||||
|
||||
const auto description_it = patch.FindMember("description");
|
||||
if (description_it != patch.MemberEnd()) {
|
||||
if (!description_it->value.IsString()
|
||||
|| has_embedded_null(description_it->value)) {
|
||||
log_warning("patchmanager", "invalid description for patch group {}", group_id);
|
||||
continue;
|
||||
}
|
||||
group.description.assign(
|
||||
description_it->value.GetString(),
|
||||
description_it->value.GetStringLength());
|
||||
}
|
||||
|
||||
const auto caution_it = patch.FindMember("caution");
|
||||
if (caution_it != patch.MemberEnd()) {
|
||||
if (!caution_it->value.IsString() || has_embedded_null(caution_it->value)) {
|
||||
log_warning("patchmanager", "invalid caution for patch group {}", group_id);
|
||||
continue;
|
||||
}
|
||||
group.caution.assign(
|
||||
caution_it->value.GetString(),
|
||||
caution_it->value.GetStringLength());
|
||||
}
|
||||
|
||||
const std::string game_code(
|
||||
game_code_it->value.GetString(),
|
||||
game_code_it->value.GetStringLength());
|
||||
if (!groups.emplace(
|
||||
make_patch_group_key(game_code, group_id),
|
||||
std::move(group)).second) {
|
||||
log_warning(
|
||||
"patchmanager",
|
||||
"duplicate patch group definition for {}/{}, ignoring duplicate",
|
||||
game_code,
|
||||
group_id);
|
||||
}
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
static const PatchGroup* find_patch_group(
|
||||
const std::map<std::pair<std::string, std::string>, PatchGroup>& groups,
|
||||
const std::string& game_code,
|
||||
const std::string& group_id) {
|
||||
const auto group = groups.find(make_patch_group_key(game_code, group_id));
|
||||
return group == groups.end() ? nullptr : &group->second;
|
||||
}
|
||||
|
||||
const PatchGroup* find_patch_group(const PatchData& patch) {
|
||||
return find_patch_group(patch_groups, patch.game_code, patch.group_id);
|
||||
}
|
||||
|
||||
std::string resolve_patch_group_id(
|
||||
const rapidjson::Value& patch,
|
||||
const std::map<std::pair<std::string, std::string>, PatchGroup>& groups,
|
||||
const std::string& game_code,
|
||||
const char *patch_name) {
|
||||
const auto group_it = patch.FindMember("group");
|
||||
if (group_it == patch.MemberEnd()) {
|
||||
return "";
|
||||
}
|
||||
if (!group_it->value.IsString()
|
||||
|| group_it->value.GetStringLength() == 0
|
||||
|| has_embedded_null(group_it->value)) {
|
||||
log_warning("patchmanager", "invalid group reference for {}", patch_name);
|
||||
return "";
|
||||
}
|
||||
|
||||
const std::string group_id(
|
||||
group_it->value.GetString(),
|
||||
group_it->value.GetStringLength());
|
||||
if (!find_patch_group(groups, game_code, group_id)) {
|
||||
log_warning(
|
||||
"patchmanager",
|
||||
"unknown patch group {}/{} referenced by {}",
|
||||
game_code,
|
||||
group_id,
|
||||
patch_name);
|
||||
return "";
|
||||
}
|
||||
|
||||
return group_id;
|
||||
}
|
||||
|
||||
void register_patch_group(
|
||||
PatchData& patch,
|
||||
const std::map<std::pair<std::string, std::string>, PatchGroup>& definitions) {
|
||||
if (patch.group_id.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto *definition = find_patch_group(
|
||||
definitions,
|
||||
patch.game_code,
|
||||
patch.group_id);
|
||||
if (!definition) {
|
||||
patch.group_id.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
const auto key = make_patch_group_key(patch.game_code, patch.group_id);
|
||||
const auto [existing, inserted] = patch_groups.emplace(key, *definition);
|
||||
if (!inserted
|
||||
&& (existing->second.name != definition->name
|
||||
|| existing->second.description != definition->description
|
||||
|| existing->second.caution != definition->caution)) {
|
||||
log_warning(
|
||||
"patchmanager",
|
||||
"conflicting group metadata for {}/{}, ignoring group on {}",
|
||||
patch.game_code,
|
||||
patch.group_id,
|
||||
patch.name);
|
||||
patch.group_id.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
@@ -74,6 +73,13 @@ namespace patcher {
|
||||
bool fatal_error = false;
|
||||
};
|
||||
|
||||
struct PatchGroup {
|
||||
std::string name;
|
||||
std::string description;
|
||||
std::string caution;
|
||||
std::string name_in_lower_case;
|
||||
};
|
||||
|
||||
struct PatchData {
|
||||
bool enabled;
|
||||
std::string game_code;
|
||||
@@ -86,6 +92,7 @@ namespace patcher {
|
||||
std::vector<MemoryPatch> patches_memory;
|
||||
std::vector<UnionPatch> patches_union;
|
||||
NumberPatch patch_number;
|
||||
std::string group_id;
|
||||
PatchStatus last_status;
|
||||
std::string hash;
|
||||
bool unverified = false;
|
||||
@@ -129,6 +136,7 @@ namespace patcher {
|
||||
|
||||
PatchStatus is_patch_active(PatchData& patch);
|
||||
bool apply_patch(PatchData& patch, bool active);
|
||||
const PatchGroup* find_patch_group(const PatchData& patch);
|
||||
|
||||
std::string displayPath(const std::filesystem::path& path);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user