mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-01 14:20:42 -07:00
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:
@@ -619,6 +619,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
patcher/config.cpp
|
patcher/config.cpp
|
||||||
patcher/lifecycle.cpp
|
patcher/lifecycle.cpp
|
||||||
patcher/loader.cpp
|
patcher/loader.cpp
|
||||||
|
patcher/loader_group.cpp
|
||||||
patcher/runtime.cpp
|
patcher/runtime.cpp
|
||||||
|
|
||||||
# external
|
# external
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <shellapi.h>
|
#include <shellapi.h>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
#include "avs/game.h"
|
#include "avs/game.h"
|
||||||
#include "cfg/configurator.h"
|
#include "cfg/configurator.h"
|
||||||
#include "games/io.h"
|
#include "games/io.h"
|
||||||
@@ -15,6 +17,363 @@
|
|||||||
|
|
||||||
namespace overlay::windows {
|
namespace overlay::windows {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
enum class PatchGroupStatus {
|
||||||
|
Error,
|
||||||
|
Disabled,
|
||||||
|
Enabled,
|
||||||
|
Mixed,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PatchGroupState {
|
||||||
|
PatchGroupStatus status = PatchGroupStatus::Disabled;
|
||||||
|
size_t first_error_index = static_cast<size_t>(-1);
|
||||||
|
size_t configured_count = 0;
|
||||||
|
size_t unverified_count = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
using PatchGroupMembers = std::unordered_map<const patcher::PatchGroup*, std::vector<size_t>>;
|
||||||
|
|
||||||
|
struct PatchGroupSearchMatch {
|
||||||
|
bool group_matches = false;
|
||||||
|
bool child_matches = false;
|
||||||
|
|
||||||
|
bool visible() const {
|
||||||
|
return group_matches || child_matches;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// aggregate child runtime and configuration state for the group UI
|
||||||
|
PatchGroupState get_patch_group_state(const std::vector<size_t>& members) {
|
||||||
|
PatchGroupState state;
|
||||||
|
bool any_enabled = false;
|
||||||
|
bool any_disabled = false;
|
||||||
|
|
||||||
|
for (const auto patch_index : members) {
|
||||||
|
const auto& patch = patcher::patches[patch_index];
|
||||||
|
if (patch.enabled) {
|
||||||
|
state.configured_count++;
|
||||||
|
}
|
||||||
|
if (patch.unverified) {
|
||||||
|
state.unverified_count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (patch.last_status) {
|
||||||
|
case patcher::PatchStatus::Error:
|
||||||
|
if (state.first_error_index == static_cast<size_t>(-1)) {
|
||||||
|
state.first_error_index = patch_index;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case patcher::PatchStatus::Enabled:
|
||||||
|
any_enabled = true;
|
||||||
|
break;
|
||||||
|
case patcher::PatchStatus::Disabled:
|
||||||
|
any_disabled = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.first_error_index != static_cast<size_t>(-1)) {
|
||||||
|
state.status = PatchGroupStatus::Error;
|
||||||
|
} else if (any_enabled && any_disabled) {
|
||||||
|
state.status = PatchGroupStatus::Mixed;
|
||||||
|
} else if (any_enabled) {
|
||||||
|
state.status = PatchGroupStatus::Enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
// index sorted patch members by their resolved group
|
||||||
|
PatchGroupMembers collect_patch_group_members() {
|
||||||
|
PatchGroupMembers group_members;
|
||||||
|
for (const auto patch_index : patcher::patches_sorted) {
|
||||||
|
const auto *group = patcher::find_patch_group(patcher::patches[patch_index]);
|
||||||
|
if (group) {
|
||||||
|
group_members[group].push_back(patch_index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return group_members;
|
||||||
|
}
|
||||||
|
|
||||||
|
// determine whether the group or any child matches the current filter
|
||||||
|
PatchGroupSearchMatch get_patch_group_search_match(
|
||||||
|
const patcher::PatchGroup& group,
|
||||||
|
const std::vector<size_t>& members,
|
||||||
|
const std::string& search_str_in_lower) {
|
||||||
|
PatchGroupSearchMatch match;
|
||||||
|
match.group_matches = search_str_in_lower.empty()
|
||||||
|
|| group.name_in_lower_case.find(search_str_in_lower) != std::string::npos;
|
||||||
|
if (match.group_matches) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto member_index : members) {
|
||||||
|
if (patcher::patches[member_index].name_in_lower_case.find(search_str_in_lower)
|
||||||
|
!= std::string::npos) {
|
||||||
|
match.child_matches = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
|
||||||
|
// select the stripe color for a logical patch or group row
|
||||||
|
ImU32 get_patch_row_background_color(size_t logical_row_index) {
|
||||||
|
const auto color = logical_row_index % 2 == 0
|
||||||
|
? ImGuiCol_TableRowBg
|
||||||
|
: ImGuiCol_TableRowBgAlt;
|
||||||
|
return ImGui::GetColorU32(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
// begin a physical table row using its logical row's stripe color
|
||||||
|
void begin_patch_row(ImU32 row_background_color) {
|
||||||
|
ImGui::TableNextRow();
|
||||||
|
ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, row_background_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw the tree branch connecting a group parent to a child row
|
||||||
|
void draw_patch_group_child_guide(
|
||||||
|
const ImVec2& cursor,
|
||||||
|
float branch_x,
|
||||||
|
float branch_end_x,
|
||||||
|
bool last_group_child) {
|
||||||
|
const auto& style = ImGui::GetStyle();
|
||||||
|
const float row_midpoint = cursor.y + ImGui::GetFrameHeight() * 0.5f;
|
||||||
|
const float row_top = cursor.y - style.CellPadding.y;
|
||||||
|
const float row_bottom = last_group_child
|
||||||
|
? row_midpoint
|
||||||
|
: cursor.y + ImGui::GetFrameHeight() + style.CellPadding.y;
|
||||||
|
const auto guide_color = ImGui::GetColorU32(ImGuiCol_Separator, 0.8f);
|
||||||
|
const float guide_thickness = overlay::apply_scaling(1.0f);
|
||||||
|
auto *draw_list = ImGui::GetWindowDrawList();
|
||||||
|
draw_list->AddLine(
|
||||||
|
ImVec2(branch_x, row_top),
|
||||||
|
ImVec2(branch_x, row_bottom),
|
||||||
|
guide_color,
|
||||||
|
guide_thickness);
|
||||||
|
draw_list->AddLine(
|
||||||
|
ImVec2(branch_x, row_midpoint),
|
||||||
|
ImVec2(branch_end_x, row_midpoint),
|
||||||
|
guide_color,
|
||||||
|
guide_thickness);
|
||||||
|
}
|
||||||
|
|
||||||
|
// reserve and render the group-child gutter in the current column
|
||||||
|
void render_patch_group_child_gutter(bool last_group_child) {
|
||||||
|
const auto& style = ImGui::GetStyle();
|
||||||
|
const auto cursor = ImGui::GetCursorScreenPos();
|
||||||
|
const float gutter_width = style.IndentSpacing * 1.5f;
|
||||||
|
const float branch_x = cursor.x + style.IndentSpacing * 0.5f;
|
||||||
|
draw_patch_group_child_guide(
|
||||||
|
cursor,
|
||||||
|
branch_x,
|
||||||
|
cursor.x + gutter_width,
|
||||||
|
last_group_child);
|
||||||
|
ImGui::Dummy(ImVec2(gutter_width, ImGui::GetFrameHeight()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// constrain the next patch option control to the available column width
|
||||||
|
void set_patch_option_width() {
|
||||||
|
const float available_width = ImGui::GetContentRegionAvail().x;
|
||||||
|
ImGui::SetNextItemWidth(available_width < 200.0f ? available_width : 200.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// show the group's help or caution marker
|
||||||
|
void show_patch_group_tooltip(const patcher::PatchGroup& group) {
|
||||||
|
if (!group.caution.empty()) {
|
||||||
|
ImGui::WarnTooltip(group.description.c_str(), group.caution.c_str());
|
||||||
|
} else if (!group.description.empty()) {
|
||||||
|
ImGui::HelpTooltip(group.description.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// append aggregate state details to the group's displayed name
|
||||||
|
std::string get_patch_group_display_name(
|
||||||
|
const patcher::PatchGroup& group,
|
||||||
|
const PatchGroupState& state,
|
||||||
|
size_t member_count) {
|
||||||
|
std::string name = group.name;
|
||||||
|
if (state.status == PatchGroupStatus::Error) {
|
||||||
|
name += " (Error)";
|
||||||
|
}
|
||||||
|
if (state.configured_count == member_count) {
|
||||||
|
name += patcher::setting_auto_apply ? " (Auto apply)" : " (Saved)";
|
||||||
|
} else if (state.configured_count > 0) {
|
||||||
|
name += patcher::setting_auto_apply
|
||||||
|
? " (Partial auto apply)"
|
||||||
|
: " (Partially saved)";
|
||||||
|
}
|
||||||
|
if (state.unverified_count == member_count) {
|
||||||
|
name += " (Unverified patches)";
|
||||||
|
} else if (state.unverified_count > 0) {
|
||||||
|
name += " (Contains unverified patch)";
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw a square checkmark for mixed-state groups (neither checked or unchecked)
|
||||||
|
void render_patch_group_mixed_checkbox_mark() {
|
||||||
|
const auto check_min = ImGui::GetItemRectMin();
|
||||||
|
const float check_size = ImGui::GetFrameHeight();
|
||||||
|
const float calculated_padding = check_size / 3.6f;
|
||||||
|
const float padding = calculated_padding < 1.0f ? 1.0f : calculated_padding;
|
||||||
|
ImGui::GetWindowDrawList()->AddRectFilled(
|
||||||
|
ImVec2(check_min.x + padding, check_min.y + padding),
|
||||||
|
ImVec2(
|
||||||
|
check_min.x + check_size - padding,
|
||||||
|
check_min.y + check_size - padding),
|
||||||
|
ImGui::GetColorU32(ImGuiCol_CheckMark),
|
||||||
|
ImGui::GetStyle().FrameRounding);
|
||||||
|
}
|
||||||
|
|
||||||
|
// render a tri-state checkbox for groups
|
||||||
|
// user can interact with this to enable or disable all child patches
|
||||||
|
bool render_patch_group_checkbox(
|
||||||
|
const PatchGroupState& state,
|
||||||
|
const std::vector<size_t>& members,
|
||||||
|
bool& checked) {
|
||||||
|
const bool mixed = state.status == PatchGroupStatus::Mixed;
|
||||||
|
ImGui::BeginDisabled(state.status == PatchGroupStatus::Error);
|
||||||
|
if (mixed) {
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_CheckMark, ImVec4(0.f, 0.f, 0.f, 0.f));
|
||||||
|
}
|
||||||
|
const bool changed = ImGui::Checkbox("##group_checked_checkbox", &checked);
|
||||||
|
if (mixed) {
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
if (mixed && !changed) {
|
||||||
|
render_patch_group_mixed_checkbox_mark();
|
||||||
|
}
|
||||||
|
if (!changed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
patcher::setting_auto_apply = true;
|
||||||
|
}
|
||||||
|
for (const auto member_index : members) {
|
||||||
|
auto& member = patcher::patches[member_index];
|
||||||
|
member.enabled = checked;
|
||||||
|
patcher::apply_patch(member, checked);
|
||||||
|
member.last_status = patcher::is_patch_active(member);
|
||||||
|
}
|
||||||
|
patcher::config_dirty = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// render aggregate status or error text beside the group checkbox
|
||||||
|
void render_patch_group_status(const PatchGroupState& state, bool checked) {
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::AlignTextToFramePadding();
|
||||||
|
if (state.status == PatchGroupStatus::Error) {
|
||||||
|
const auto& error_patch = patcher::patches[state.first_error_index];
|
||||||
|
const auto error_reason = error_patch.error_reason.empty()
|
||||||
|
? "Unknown error"
|
||||||
|
: error_patch.error_reason;
|
||||||
|
const auto error_text = error_patch.name + ": " + error_reason;
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.f, 0.f, 0.f, 1.f));
|
||||||
|
ImGui::TextUnformatted(error_text.c_str());
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
} else if (state.status == PatchGroupStatus::Mixed) {
|
||||||
|
ImGui::TextUnformatted("mixed");
|
||||||
|
} else {
|
||||||
|
ImGui::BeginDisabled(!checked);
|
||||||
|
ImGui::TextUnformatted(checked ? "ON" : "off");
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// render a group parent row and report whether its children are expanded
|
||||||
|
bool render_patch_group_parent(
|
||||||
|
const patcher::PatchGroup& group,
|
||||||
|
const std::vector<size_t>& members,
|
||||||
|
bool child_matches_search,
|
||||||
|
ImU32 row_background_color) {
|
||||||
|
auto group_state = get_patch_group_state(members);
|
||||||
|
|
||||||
|
begin_patch_row(row_background_color);
|
||||||
|
// render status first so its checkbox can set the tree's next open state
|
||||||
|
ImGui::TableSetColumnIndex(1);
|
||||||
|
bool group_checked = group_state.status == PatchGroupStatus::Enabled
|
||||||
|
|| group_state.status == PatchGroupStatus::Mixed;
|
||||||
|
const bool group_checkbox_changed = render_patch_group_checkbox(
|
||||||
|
group_state,
|
||||||
|
members,
|
||||||
|
group_checked);
|
||||||
|
if (group_checkbox_changed) {
|
||||||
|
group_state = get_patch_group_state(members);
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered(ImGui::TOOLTIP_FLAGS)) {
|
||||||
|
show_patch_group_tooltip(group);
|
||||||
|
}
|
||||||
|
render_patch_group_status(group_state, group_checked);
|
||||||
|
|
||||||
|
ImGui::TableSetColumnIndex(0);
|
||||||
|
const auto group_name = get_patch_group_display_name(
|
||||||
|
group,
|
||||||
|
group_state,
|
||||||
|
members.size());
|
||||||
|
bool group_text_color_set = false;
|
||||||
|
ImVec4 group_text_color;
|
||||||
|
switch (group_state.status) {
|
||||||
|
case PatchGroupStatus::Error:
|
||||||
|
group_text_color = ImVec4(1.f, 0.f, 0.f, 1.f);
|
||||||
|
group_text_color_set = true;
|
||||||
|
break;
|
||||||
|
case PatchGroupStatus::Enabled:
|
||||||
|
if (patcher::setting_auto_apply
|
||||||
|
&& group_state.configured_count == members.size()) {
|
||||||
|
group_text_color = ImVec4(0.f, 1.f, 0.f, 1.f);
|
||||||
|
group_text_color_set = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case PatchGroupStatus::Mixed:
|
||||||
|
group_text_color = ImVec4(0.f, 1.f, 0.f, 1.f);
|
||||||
|
group_text_color_set = true;
|
||||||
|
break;
|
||||||
|
case PatchGroupStatus::Disabled:
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!group.caution.empty()) {
|
||||||
|
ImGui::AlignTextToFramePadding();
|
||||||
|
ImGui::WarnMarker(group.description.c_str(), group.caution.c_str());
|
||||||
|
} else {
|
||||||
|
ImGui::DummyMarker();
|
||||||
|
}
|
||||||
|
ImGui::SameLine(0.0f, 0.0f);
|
||||||
|
ImGui::AlignTextToFramePadding();
|
||||||
|
if (child_matches_search) {
|
||||||
|
ImGui::SetNextItemOpen(true, ImGuiCond_Always);
|
||||||
|
} else if (group_checkbox_changed) {
|
||||||
|
ImGui::SetNextItemOpen(group_checked, ImGuiCond_Always);
|
||||||
|
}
|
||||||
|
if (group_text_color_set) {
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, group_text_color);
|
||||||
|
}
|
||||||
|
bool group_open = ImGui::TreeNodeEx(
|
||||||
|
"##group_tree",
|
||||||
|
ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_NoTreePushOnOpen,
|
||||||
|
"%s",
|
||||||
|
group_name.c_str());
|
||||||
|
if (group_text_color_set) {
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered(ImGui::TOOLTIP_FLAGS)) {
|
||||||
|
show_patch_group_tooltip(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::HighlightTableRowOnHover();
|
||||||
|
return group_open;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// user-assigned IDs for the patches table columns, used by the sort logic
|
// user-assigned IDs for the patches table columns, used by the sort logic
|
||||||
enum PatchColumnId {
|
enum PatchColumnId {
|
||||||
PATCH_COLUMN_NAME = 0,
|
PATCH_COLUMN_NAME = 0,
|
||||||
@@ -406,49 +765,42 @@ namespace overlay::windows {
|
|||||||
240, PATCH_COLUMN_STATUS);
|
240, PATCH_COLUMN_STATUS);
|
||||||
ImGui::TableHeadersRow();
|
ImGui::TableHeadersRow();
|
||||||
|
|
||||||
// maintain a sorted view of patches so the underlying vector
|
// refresh every child before sorting and aggregating group state
|
||||||
// order (used by config save and hashing) is left untouched
|
for (auto& patch : patcher::patches) {
|
||||||
update_sorted_patches();
|
patch.last_status = patcher::is_patch_active(patch);
|
||||||
|
|
||||||
const auto search_str_in_lower = strtolower(patch_name_filter);
|
|
||||||
size_t patches_shown = 0;
|
|
||||||
for (auto patch_index : patcher::patches_sorted) {
|
|
||||||
auto &patch = patcher::patches[patch_index];
|
|
||||||
|
|
||||||
// get patch status
|
|
||||||
patcher::PatchStatus patch_status = patcher::is_patch_active(patch);
|
|
||||||
patch.last_status = patch_status;
|
|
||||||
|
|
||||||
// user requested to disable all
|
|
||||||
if (disable_all_patches && patch.enabled) {
|
if (disable_all_patches && patch.enabled) {
|
||||||
patch.enabled = false;
|
patch.enabled = false;
|
||||||
patcher::config_dirty = true;
|
patcher::config_dirty = true;
|
||||||
switch (patch_status) {
|
switch (patch.last_status) {
|
||||||
case patcher::PatchStatus::Enabled:
|
case patcher::PatchStatus::Enabled:
|
||||||
case patcher::PatchStatus::Disabled:
|
case patcher::PatchStatus::Disabled:
|
||||||
patcher::apply_patch(patch, false);
|
patcher::apply_patch(patch, false);
|
||||||
|
patch.last_status = patcher::is_patch_active(patch);
|
||||||
break;
|
break;
|
||||||
case patcher::PatchStatus::Error:
|
case patcher::PatchStatus::Error:
|
||||||
if (cfg::CONFIGURATOR_STANDALONE) {
|
|
||||||
patch.enabled = false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// search function
|
// maintain a sorted view of patches so the underlying vector
|
||||||
if (!patch_name_filter.empty()) {
|
// order (used by config save and hashing) is left untouched
|
||||||
if (patch.name_in_lower_case.find(search_str_in_lower) == std::string::npos) {
|
update_sorted_patches();
|
||||||
continue;
|
|
||||||
}
|
// function for rendering a patch row, used for both grouped and ungrouped patches
|
||||||
}
|
const auto search_str_in_lower = strtolower(patch_name_filter);
|
||||||
|
size_t items_shown = 0;
|
||||||
|
auto render_patch = [&](size_t patch_index, ImU32 row_background_color,
|
||||||
|
bool group_child = false,
|
||||||
|
bool last_group_child = false) {
|
||||||
|
auto &patch = patcher::patches[patch_index];
|
||||||
|
const patcher::PatchStatus patch_status = patch.last_status;
|
||||||
|
|
||||||
// start drawing a row for this patch
|
// start drawing a row for this patch
|
||||||
ImGui::TableNextRow();
|
begin_patch_row(row_background_color);
|
||||||
ImGui::PushID(&patch);
|
ImGui::PushID(&patch);
|
||||||
patches_shown += 1;
|
|
||||||
|
|
||||||
// first column, part 1: help / caution marker
|
// first column, part 1: help / caution marker
|
||||||
ImGui::TableNextColumn();
|
ImGui::TableNextColumn();
|
||||||
@@ -458,6 +810,10 @@ namespace overlay::windows {
|
|||||||
} else {
|
} else {
|
||||||
ImGui::DummyMarker();
|
ImGui::DummyMarker();
|
||||||
}
|
}
|
||||||
|
if (group_child) {
|
||||||
|
ImGui::SameLine();
|
||||||
|
render_patch_group_child_gutter(last_group_child);
|
||||||
|
}
|
||||||
|
|
||||||
// get current state
|
// get current state
|
||||||
bool patch_checked = patch_status == patcher::PatchStatus::Enabled;
|
bool patch_checked = patch_status == patcher::PatchStatus::Enabled;
|
||||||
@@ -514,6 +870,10 @@ namespace overlay::windows {
|
|||||||
|
|
||||||
// second column, part 1: enable checkbox (applies to all)
|
// second column, part 1: enable checkbox (applies to all)
|
||||||
ImGui::TableNextColumn();
|
ImGui::TableNextColumn();
|
||||||
|
if (group_child) {
|
||||||
|
render_patch_group_child_gutter(last_group_child);
|
||||||
|
ImGui::SameLine();
|
||||||
|
}
|
||||||
ImGui::BeginDisabled(patch_status == patcher::PatchStatus::Error);
|
ImGui::BeginDisabled(patch_status == patcher::PatchStatus::Error);
|
||||||
if (ImGui::Checkbox("##patch_checked_checkbox", &patch_checked)) {
|
if (ImGui::Checkbox("##patch_checked_checkbox", &patch_checked)) {
|
||||||
patcher::config_dirty = true;
|
patcher::config_dirty = true;
|
||||||
@@ -559,7 +919,7 @@ namespace overlay::windows {
|
|||||||
} else if (patch.type == patcher::PatchType::Union || patch.type == patcher::PatchType::Integer) {
|
} else if (patch.type == patcher::PatchType::Union || patch.type == patcher::PatchType::Integer) {
|
||||||
if (patch_status == patcher::PatchStatus::Enabled) {
|
if (patch_status == patcher::PatchStatus::Enabled) {
|
||||||
if (patch.type == patcher::PatchType::Union) {
|
if (patch.type == patcher::PatchType::Union) {
|
||||||
ImGui::SetNextItemWidth(200.0f);
|
set_patch_option_width();
|
||||||
if (ImGui::BeginCombo("##union_patch_dropdown", patch.selected_union_name.c_str())) {
|
if (ImGui::BeginCombo("##union_patch_dropdown", patch.selected_union_name.c_str())) {
|
||||||
for (const auto& union_patch : patch.patches_union) {
|
for (const auto& union_patch : patch.patches_union) {
|
||||||
if (ImGui::Selectable(union_patch.name.c_str())) {
|
if (ImGui::Selectable(union_patch.name.c_str())) {
|
||||||
@@ -574,7 +934,7 @@ namespace overlay::windows {
|
|||||||
show_patch_tooltip(patch);
|
show_patch_tooltip(patch);
|
||||||
}
|
}
|
||||||
} else if (patch.type == patcher::PatchType::Integer) {
|
} else if (patch.type == patcher::PatchType::Integer) {
|
||||||
ImGui::SetNextItemWidth(200.0f);
|
set_patch_option_width();
|
||||||
auto& numpatch = patch.patch_number;
|
auto& numpatch = patch.patch_number;
|
||||||
ImGui::InputInt("##int_input", &numpatch.value, 1, 10);
|
ImGui::InputInt("##int_input", &numpatch.value, 1, 10);
|
||||||
if (ImGui::IsItemDeactivatedAfterEdit()) {
|
if (ImGui::IsItemDeactivatedAfterEdit()) {
|
||||||
@@ -591,7 +951,7 @@ namespace overlay::windows {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (patch_status == patcher::PatchStatus::Disabled) {
|
} else if (patch_status == patcher::PatchStatus::Disabled) {
|
||||||
ImGui::SetNextItemWidth(200.0f);
|
set_patch_option_width();
|
||||||
ImGui::BeginDisabled();
|
ImGui::BeginDisabled();
|
||||||
if (patch.type == patcher::PatchType::Union) {
|
if (patch.type == patcher::PatchType::Union) {
|
||||||
if (ImGui::BeginCombo(
|
if (ImGui::BeginCombo(
|
||||||
@@ -616,10 +976,65 @@ namespace overlay::windows {
|
|||||||
|
|
||||||
ImGui::HighlightTableRowOnHover();
|
ImGui::HighlightTableRowOnHover();
|
||||||
|
|
||||||
|
ImGui::PopID();
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto group_members = collect_patch_group_members();
|
||||||
|
std::unordered_set<const patcher::PatchGroup*> rendered_groups;
|
||||||
|
|
||||||
|
// time to render each row using render_patch
|
||||||
|
for (const auto patch_index : patcher::patches_sorted) {
|
||||||
|
auto& patch = patcher::patches[patch_index];
|
||||||
|
const auto *group = patcher::find_patch_group(patch);
|
||||||
|
if (!group) {
|
||||||
|
if (!patch_name_filter.empty()
|
||||||
|
&& patch.name_in_lower_case.find(search_str_in_lower) == std::string::npos) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto row_background_color =
|
||||||
|
get_patch_row_background_color(items_shown++);
|
||||||
|
render_patch(patch_index, row_background_color);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rendered_groups.insert(group).second) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& members = group_members.at(group);
|
||||||
|
const auto search_match = get_patch_group_search_match(
|
||||||
|
*group,
|
||||||
|
members,
|
||||||
|
search_str_in_lower);
|
||||||
|
if (!search_match.visible()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::PushID(group);
|
||||||
|
const auto row_background_color =
|
||||||
|
get_patch_row_background_color(items_shown++);
|
||||||
|
const bool group_open = render_patch_group_parent(
|
||||||
|
*group,
|
||||||
|
members,
|
||||||
|
search_match.child_matches,
|
||||||
|
row_background_color);
|
||||||
|
if (group_open) {
|
||||||
|
for (size_t member_position = 0;
|
||||||
|
member_position < members.size();
|
||||||
|
member_position++) {
|
||||||
|
render_patch(
|
||||||
|
members[member_position],
|
||||||
|
row_background_color,
|
||||||
|
true,
|
||||||
|
member_position + 1 == members.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (patches_shown == 0) {
|
// no patches, show empty table
|
||||||
|
if (items_shown == 0) {
|
||||||
ImGui::TableNextRow();
|
ImGui::TableNextRow();
|
||||||
ImGui::TableNextColumn();
|
ImGui::TableNextColumn();
|
||||||
ImGui::DummyMarker();
|
ImGui::DummyMarker();
|
||||||
@@ -644,44 +1059,86 @@ namespace overlay::windows {
|
|||||||
// clears the cache to avoid dangling pointers when `patches` is rebuilt.
|
// clears the cache to avoid dangling pointers when `patches` is rebuilt.
|
||||||
auto *sort_specs = ImGui::TableGetSortSpecs();
|
auto *sort_specs = ImGui::TableGetSortSpecs();
|
||||||
const bool patches_changed = patcher::patches_sorted.size() != patcher::patches.size();
|
const bool patches_changed = patcher::patches_sorted.size() != patcher::patches.size();
|
||||||
if (!patches_changed && !(sort_specs && sort_specs->SpecsDirty)) {
|
const bool status_sort_active = sort_specs
|
||||||
|
&& sort_specs->SpecsCount > 0
|
||||||
|
&& sort_specs->Specs[0].ColumnUserID == PATCH_COLUMN_STATUS;
|
||||||
|
if (!patches_changed && !status_sort_active && !(sort_specs && sort_specs->SpecsDirty)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// rebuild the view in the underlying vector order (this is also the
|
struct SortItem {
|
||||||
// default order shown when the sort is cleared / tristate "unsorted")
|
std::vector<size_t> members;
|
||||||
patcher::patches_sorted.clear();
|
std::string name_in_lower_case;
|
||||||
patcher::patches_sorted.reserve(patcher::patches.size());
|
bool enabled;
|
||||||
|
};
|
||||||
|
|
||||||
|
PatchGroupMembers group_members;
|
||||||
|
std::vector<const patcher::PatchGroup*> groups_by_patch(patcher::patches.size());
|
||||||
for (size_t i = 0; i < patcher::patches.size(); i++) {
|
for (size_t i = 0; i < patcher::patches.size(); i++) {
|
||||||
patcher::patches_sorted.push_back(i);
|
const auto *group = patcher::find_patch_group(patcher::patches[i]);
|
||||||
|
groups_by_patch[i] = group;
|
||||||
|
if (group) {
|
||||||
|
group_members[group].push_back(i);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SpecsCount == 0 means no active sort: keep the default order
|
// build one sortable item per ungrouped patch or group, placing each
|
||||||
|
// group at its first occurrence in the underlying file order
|
||||||
|
std::vector<SortItem> sort_items;
|
||||||
|
sort_items.reserve(patcher::patches.size());
|
||||||
|
std::unordered_set<const patcher::PatchGroup*> emitted_groups;
|
||||||
|
for (size_t i = 0; i < patcher::patches.size(); i++) {
|
||||||
|
const auto& patch = patcher::patches[i];
|
||||||
|
const auto *group = groups_by_patch[i];
|
||||||
|
if (!group) {
|
||||||
|
sort_items.push_back({
|
||||||
|
.members = {i},
|
||||||
|
.name_in_lower_case = patch.name_in_lower_case,
|
||||||
|
.enabled = patch.last_status == patcher::PatchStatus::Enabled,
|
||||||
|
});
|
||||||
|
} else if (emitted_groups.insert(group).second) {
|
||||||
|
auto members = std::move(group_members.at(group));
|
||||||
|
const bool group_enabled = get_patch_group_state(members).status
|
||||||
|
== PatchGroupStatus::Enabled;
|
||||||
|
sort_items.push_back({
|
||||||
|
.members = std::move(members),
|
||||||
|
.name_in_lower_case = group->name_in_lower_case,
|
||||||
|
.enabled = group_enabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// no active sort specs means the default item order is retained
|
||||||
if (sort_specs && sort_specs->SpecsCount > 0) {
|
if (sort_specs && sort_specs->SpecsCount > 0) {
|
||||||
const auto &spec = sort_specs->Specs[0];
|
const auto &spec = sort_specs->Specs[0];
|
||||||
const bool ascending = spec.SortDirection != ImGuiSortDirection_Descending;
|
const bool ascending = spec.SortDirection != ImGuiSortDirection_Descending;
|
||||||
std::stable_sort(patcher::patches_sorted.begin(), patcher::patches_sorted.end(),
|
std::stable_sort(sort_items.begin(), sort_items.end(),
|
||||||
[&](size_t ia, size_t ib) {
|
[&](const SortItem& a, const SortItem& b) {
|
||||||
const patcher::PatchData *a = &patcher::patches[ia];
|
|
||||||
const patcher::PatchData *b = &patcher::patches[ib];
|
|
||||||
int cmp;
|
int cmp;
|
||||||
if (spec.ColumnUserID == PATCH_COLUMN_STATUS) {
|
if (spec.ColumnUserID == PATCH_COLUMN_STATUS) {
|
||||||
// sort by displayed status (matches the checkbox), then
|
// sort by displayed status (matches the checkbox), then
|
||||||
// by name as a tiebreaker
|
// by name as a tiebreaker
|
||||||
const bool a_on = a->last_status == patcher::PatchStatus::Enabled;
|
if (a.enabled != b.enabled) {
|
||||||
const bool b_on = b->last_status == patcher::PatchStatus::Enabled;
|
cmp = a.enabled ? -1 : 1;
|
||||||
if (a_on != b_on) {
|
|
||||||
cmp = a_on ? -1 : 1;
|
|
||||||
} else {
|
} else {
|
||||||
cmp = a->name_in_lower_case.compare(b->name_in_lower_case);
|
cmp = a.name_in_lower_case.compare(b.name_in_lower_case);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
cmp = a->name_in_lower_case.compare(b->name_in_lower_case);
|
cmp = a.name_in_lower_case.compare(b.name_in_lower_case);
|
||||||
}
|
}
|
||||||
return ascending ? (cmp < 0) : (cmp > 0);
|
return ascending ? (cmp < 0) : (cmp > 0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
patcher::patches_sorted.clear();
|
||||||
|
patcher::patches_sorted.reserve(patcher::patches.size());
|
||||||
|
for (const auto& item : sort_items) {
|
||||||
|
patcher::patches_sorted.insert(
|
||||||
|
patcher::patches_sorted.end(),
|
||||||
|
item.members.begin(),
|
||||||
|
item.members.end());
|
||||||
|
}
|
||||||
|
|
||||||
if (sort_specs) {
|
if (sort_specs) {
|
||||||
sort_specs->SpecsDirty = false;
|
sort_specs->SpecsDirty = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
#include "patch_manager.h"
|
#include "patch_manager.h"
|
||||||
#include "external/rapidjson/document.h"
|
#include "external/rapidjson/document.h"
|
||||||
#include "util/nt_loader.h"
|
#include "util/nt_loader.h"
|
||||||
@@ -11,6 +14,7 @@ namespace patcher {
|
|||||||
extern std::vector<std::string> setting_patches_enabled;
|
extern std::vector<std::string> setting_patches_enabled;
|
||||||
extern std::map<std::string, std::string> setting_union_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::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::filesystem::path LOCAL_PATCHES_PATH;
|
||||||
extern std::map<std::string, std::vector<std::string>> EXTRA_DLLS;
|
extern std::map<std::string, std::vector<std::string>> EXTRA_DLLS;
|
||||||
extern bool ldr_registered;
|
extern bool ldr_registered;
|
||||||
@@ -28,6 +32,17 @@ namespace patcher {
|
|||||||
bool load_from_patches_json(bool apply_patches);
|
bool load_from_patches_json(bool apply_patches);
|
||||||
void load_embedded_patches(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 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);
|
VOID CALLBACK loader_notification(ULONG reason, PCLDR_DLL_NOTIFICATION_DATA data, PVOID context);
|
||||||
|
|
||||||
std::string patch_hash(PatchData& patch);
|
std::string patch_hash(PatchData& patch);
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ namespace patcher {
|
|||||||
std::vector<PatchData> patches;
|
std::vector<PatchData> patches;
|
||||||
bool local_patches_initialized = false;
|
bool local_patches_initialized = false;
|
||||||
std::vector<size_t> patches_sorted;
|
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 = {
|
std::map<std::string, std::vector<std::string>> EXTRA_DLLS = {
|
||||||
{"jubeat.dll", {"music_db.dll", "coin.dll"}},
|
{"jubeat.dll", {"music_db.dll", "coin.dll"}},
|
||||||
{"arkmdxp3.dll", {"gamemdx.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));
|
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
|
// iterate patches
|
||||||
for (auto &patch : doc.GetArray()) {
|
for (auto &patch : doc.GetArray()) {
|
||||||
|
if (is_patch_group_definition(patch)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// verfiy patch data
|
// verfiy patch data
|
||||||
auto name_it = patch.FindMember("name");
|
auto name_it = patch.FindMember("name");
|
||||||
@@ -214,6 +219,9 @@ namespace patcher {
|
|||||||
name_it->value.GetString());
|
name_it->value.GetString());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const std::string game_code(
|
||||||
|
game_code_it->value.GetString(),
|
||||||
|
game_code_it->value.GetStringLength());
|
||||||
auto description_it = patch.FindMember("description");
|
auto description_it = patch.FindMember("description");
|
||||||
if (description_it == patch.MemberEnd() || !description_it->value.IsString()) {
|
if (description_it == patch.MemberEnd() || !description_it->value.IsString()) {
|
||||||
log_warning("patchmanager", "failed to parse description for {}",
|
log_warning("patchmanager", "failed to parse description for {}",
|
||||||
@@ -235,7 +243,7 @@ namespace patcher {
|
|||||||
// build patch data
|
// build patch data
|
||||||
PatchData patch_data {
|
PatchData patch_data {
|
||||||
.enabled = false,
|
.enabled = false,
|
||||||
.game_code = game_code_it->value.GetString(),
|
.game_code = game_code,
|
||||||
.datecode_min = 0,
|
.datecode_min = 0,
|
||||||
.datecode_max = 0,
|
.datecode_max = 0,
|
||||||
.name = name_it->value.GetString(),
|
.name = name_it->value.GetString(),
|
||||||
@@ -247,6 +255,11 @@ namespace patcher {
|
|||||||
.patches_memory = std::vector<MemoryPatch>(),
|
.patches_memory = std::vector<MemoryPatch>(),
|
||||||
.patches_union = std::vector<UnionPatch>(),
|
.patches_union = std::vector<UnionPatch>(),
|
||||||
.patch_number = NumberPatch(),
|
.patch_number = NumberPatch(),
|
||||||
|
.group_id = resolve_patch_group_id(
|
||||||
|
patch,
|
||||||
|
group_definitions,
|
||||||
|
game_code,
|
||||||
|
name_it->value.GetString()),
|
||||||
.last_status = PatchStatus::Disabled,
|
.last_status = PatchStatus::Disabled,
|
||||||
.hash = "",
|
.hash = "",
|
||||||
.unverified = false,
|
.unverified = false,
|
||||||
@@ -533,6 +546,7 @@ namespace patcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// auto apply
|
// auto apply
|
||||||
|
register_patch_group(patch_data, group_definitions);
|
||||||
if (apply_patches && setting_auto_apply && patch_data.enabled) {
|
if (apply_patches && setting_auto_apply && patch_data.enabled) {
|
||||||
print_auto_apply_status(patch_data);
|
print_auto_apply_status(patch_data);
|
||||||
apply_patch(patch_data, true);
|
apply_patch(patch_data, true);
|
||||||
@@ -663,6 +677,7 @@ namespace patcher {
|
|||||||
|
|
||||||
// clear old patches
|
// clear old patches
|
||||||
patches.clear();
|
patches.clear();
|
||||||
|
patch_groups.clear();
|
||||||
// drop the cached sorted view so the table rebuilds it (in default order)
|
// drop the cached sorted view so the table rebuilds it (in default order)
|
||||||
// on the next frame
|
// on the next frame
|
||||||
patches_sorted.clear();
|
patches_sorted.clear();
|
||||||
@@ -719,6 +734,7 @@ namespace patcher {
|
|||||||
bool imported = false;
|
bool imported = false;
|
||||||
// clear old patches
|
// clear old patches
|
||||||
patches.clear();
|
patches.clear();
|
||||||
|
patch_groups.clear();
|
||||||
patches_sorted.clear();
|
patches_sorted.clear();
|
||||||
url_fetch_errors.clear();
|
url_fetch_errors.clear();
|
||||||
|
|
||||||
@@ -755,8 +771,13 @@ namespace patcher {
|
|||||||
rapidjson::GetParseError_En(error));
|
rapidjson::GetParseError_En(error));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const auto group_definitions = parse_patch_group_definitions(doc);
|
||||||
|
|
||||||
// iterate patches
|
// iterate patches
|
||||||
for (auto &patch : doc.GetArray()) {
|
for (auto &patch : doc.GetArray()) {
|
||||||
|
if (is_patch_group_definition(patch)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// verfiy patch data
|
// verfiy patch data
|
||||||
auto name_it = patch.FindMember("name");
|
auto name_it = patch.FindMember("name");
|
||||||
@@ -778,6 +799,9 @@ namespace patcher {
|
|||||||
name_it->value.GetString());
|
name_it->value.GetString());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const std::string game_code(
|
||||||
|
game_code_it->value.GetString(),
|
||||||
|
game_code_it->value.GetStringLength());
|
||||||
auto description_it = patch.FindMember("description");
|
auto description_it = patch.FindMember("description");
|
||||||
if (description_it == patch.MemberEnd() || !description_it->value.IsString()) {
|
if (description_it == patch.MemberEnd() || !description_it->value.IsString()) {
|
||||||
log_warning("patchmanager", "failed to parse description for {}",
|
log_warning("patchmanager", "failed to parse description for {}",
|
||||||
@@ -809,7 +833,7 @@ namespace patcher {
|
|||||||
// build patch data
|
// build patch data
|
||||||
PatchData patch_data {
|
PatchData patch_data {
|
||||||
.enabled = false,
|
.enabled = false,
|
||||||
.game_code = game_code_it->value.GetString(),
|
.game_code = game_code,
|
||||||
.datecode_min = 0,
|
.datecode_min = 0,
|
||||||
.datecode_max = 0,
|
.datecode_max = 0,
|
||||||
.name = name_it->value.GetString(),
|
.name = name_it->value.GetString(),
|
||||||
@@ -821,6 +845,11 @@ namespace patcher {
|
|||||||
.patches_memory = std::vector<MemoryPatch>(),
|
.patches_memory = std::vector<MemoryPatch>(),
|
||||||
.patches_union = std::vector<UnionPatch>(),
|
.patches_union = std::vector<UnionPatch>(),
|
||||||
.patch_number = NumberPatch(),
|
.patch_number = NumberPatch(),
|
||||||
|
.group_id = resolve_patch_group_id(
|
||||||
|
patch,
|
||||||
|
group_definitions,
|
||||||
|
game_code,
|
||||||
|
name_it->value.GetString()),
|
||||||
.last_status = PatchStatus::Disabled,
|
.last_status = PatchStatus::Disabled,
|
||||||
.hash = "",
|
.hash = "",
|
||||||
.unverified = false,
|
.unverified = false,
|
||||||
@@ -1264,6 +1293,7 @@ namespace patcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// auto apply
|
// auto apply
|
||||||
|
register_patch_group(patch_data, group_definitions);
|
||||||
if (apply_patches && setting_auto_apply && patch_data.enabled) {
|
if (apply_patches && setting_auto_apply && patch_data.enabled) {
|
||||||
print_auto_apply_status(patch_data);
|
print_auto_apply_status(patch_data);
|
||||||
apply_patch(patch_data, true);
|
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 <filesystem>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <map>
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -74,6 +73,13 @@ namespace patcher {
|
|||||||
bool fatal_error = false;
|
bool fatal_error = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct PatchGroup {
|
||||||
|
std::string name;
|
||||||
|
std::string description;
|
||||||
|
std::string caution;
|
||||||
|
std::string name_in_lower_case;
|
||||||
|
};
|
||||||
|
|
||||||
struct PatchData {
|
struct PatchData {
|
||||||
bool enabled;
|
bool enabled;
|
||||||
std::string game_code;
|
std::string game_code;
|
||||||
@@ -86,6 +92,7 @@ namespace patcher {
|
|||||||
std::vector<MemoryPatch> patches_memory;
|
std::vector<MemoryPatch> patches_memory;
|
||||||
std::vector<UnionPatch> patches_union;
|
std::vector<UnionPatch> patches_union;
|
||||||
NumberPatch patch_number;
|
NumberPatch patch_number;
|
||||||
|
std::string group_id;
|
||||||
PatchStatus last_status;
|
PatchStatus last_status;
|
||||||
std::string hash;
|
std::string hash;
|
||||||
bool unverified = false;
|
bool unverified = false;
|
||||||
@@ -129,6 +136,7 @@ namespace patcher {
|
|||||||
|
|
||||||
PatchStatus is_patch_active(PatchData& patch);
|
PatchStatus is_patch_active(PatchData& patch);
|
||||||
bool apply_patch(PatchData& patch, bool active);
|
bool apply_patch(PatchData& patch, bool active);
|
||||||
|
const PatchGroup* find_patch_group(const PatchData& patch);
|
||||||
|
|
||||||
std::string displayPath(const std::filesystem::path& path);
|
std::string displayPath(const std::filesystem::path& path);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user