mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-02 23:00: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:
@@ -3,6 +3,8 @@
|
||||
#include <algorithm>
|
||||
#include <shellapi.h>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include "avs/game.h"
|
||||
#include "cfg/configurator.h"
|
||||
#include "games/io.h"
|
||||
@@ -15,6 +17,363 @@
|
||||
|
||||
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
|
||||
enum PatchColumnId {
|
||||
PATCH_COLUMN_NAME = 0,
|
||||
@@ -406,49 +765,42 @@ namespace overlay::windows {
|
||||
240, PATCH_COLUMN_STATUS);
|
||||
ImGui::TableHeadersRow();
|
||||
|
||||
// maintain a sorted view of patches so the underlying vector
|
||||
// order (used by config save and hashing) is left untouched
|
||||
update_sorted_patches();
|
||||
// refresh every child before sorting and aggregating group state
|
||||
for (auto& patch : patcher::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) {
|
||||
patch.enabled = false;
|
||||
patcher::config_dirty = true;
|
||||
switch (patch_status) {
|
||||
switch (patch.last_status) {
|
||||
case patcher::PatchStatus::Enabled:
|
||||
case patcher::PatchStatus::Disabled:
|
||||
patcher::apply_patch(patch, false);
|
||||
patch.last_status = patcher::is_patch_active(patch);
|
||||
break;
|
||||
case patcher::PatchStatus::Error:
|
||||
if (cfg::CONFIGURATOR_STANDALONE) {
|
||||
patch.enabled = false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// search function
|
||||
if (!patch_name_filter.empty()) {
|
||||
if (patch.name_in_lower_case.find(search_str_in_lower) == std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// maintain a sorted view of patches so the underlying vector
|
||||
// order (used by config save and hashing) is left untouched
|
||||
update_sorted_patches();
|
||||
|
||||
// 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
|
||||
ImGui::TableNextRow();
|
||||
begin_patch_row(row_background_color);
|
||||
ImGui::PushID(&patch);
|
||||
patches_shown += 1;
|
||||
|
||||
// first column, part 1: help / caution marker
|
||||
ImGui::TableNextColumn();
|
||||
@@ -458,6 +810,10 @@ namespace overlay::windows {
|
||||
} else {
|
||||
ImGui::DummyMarker();
|
||||
}
|
||||
if (group_child) {
|
||||
ImGui::SameLine();
|
||||
render_patch_group_child_gutter(last_group_child);
|
||||
}
|
||||
|
||||
// get current state
|
||||
bool patch_checked = patch_status == patcher::PatchStatus::Enabled;
|
||||
@@ -514,6 +870,10 @@ namespace overlay::windows {
|
||||
|
||||
// second column, part 1: enable checkbox (applies to all)
|
||||
ImGui::TableNextColumn();
|
||||
if (group_child) {
|
||||
render_patch_group_child_gutter(last_group_child);
|
||||
ImGui::SameLine();
|
||||
}
|
||||
ImGui::BeginDisabled(patch_status == patcher::PatchStatus::Error);
|
||||
if (ImGui::Checkbox("##patch_checked_checkbox", &patch_checked)) {
|
||||
patcher::config_dirty = true;
|
||||
@@ -559,7 +919,7 @@ namespace overlay::windows {
|
||||
} else if (patch.type == patcher::PatchType::Union || patch.type == patcher::PatchType::Integer) {
|
||||
if (patch_status == patcher::PatchStatus::Enabled) {
|
||||
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())) {
|
||||
for (const auto& union_patch : patch.patches_union) {
|
||||
if (ImGui::Selectable(union_patch.name.c_str())) {
|
||||
@@ -574,7 +934,7 @@ namespace overlay::windows {
|
||||
show_patch_tooltip(patch);
|
||||
}
|
||||
} else if (patch.type == patcher::PatchType::Integer) {
|
||||
ImGui::SetNextItemWidth(200.0f);
|
||||
set_patch_option_width();
|
||||
auto& numpatch = patch.patch_number;
|
||||
ImGui::InputInt("##int_input", &numpatch.value, 1, 10);
|
||||
if (ImGui::IsItemDeactivatedAfterEdit()) {
|
||||
@@ -591,7 +951,7 @@ namespace overlay::windows {
|
||||
}
|
||||
}
|
||||
} else if (patch_status == patcher::PatchStatus::Disabled) {
|
||||
ImGui::SetNextItemWidth(200.0f);
|
||||
set_patch_option_width();
|
||||
ImGui::BeginDisabled();
|
||||
if (patch.type == patcher::PatchType::Union) {
|
||||
if (ImGui::BeginCombo(
|
||||
@@ -616,10 +976,65 @@ namespace overlay::windows {
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
if (patches_shown == 0) {
|
||||
// no patches, show empty table
|
||||
if (items_shown == 0) {
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::DummyMarker();
|
||||
@@ -644,44 +1059,86 @@ namespace overlay::windows {
|
||||
// clears the cache to avoid dangling pointers when `patches` is rebuilt.
|
||||
auto *sort_specs = ImGui::TableGetSortSpecs();
|
||||
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;
|
||||
}
|
||||
|
||||
// rebuild the view in the underlying vector order (this is also the
|
||||
// default order shown when the sort is cleared / tristate "unsorted")
|
||||
patcher::patches_sorted.clear();
|
||||
patcher::patches_sorted.reserve(patcher::patches.size());
|
||||
struct SortItem {
|
||||
std::vector<size_t> members;
|
||||
std::string name_in_lower_case;
|
||||
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++) {
|
||||
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) {
|
||||
const auto &spec = sort_specs->Specs[0];
|
||||
const bool ascending = spec.SortDirection != ImGuiSortDirection_Descending;
|
||||
std::stable_sort(patcher::patches_sorted.begin(), patcher::patches_sorted.end(),
|
||||
[&](size_t ia, size_t ib) {
|
||||
const patcher::PatchData *a = &patcher::patches[ia];
|
||||
const patcher::PatchData *b = &patcher::patches[ib];
|
||||
std::stable_sort(sort_items.begin(), sort_items.end(),
|
||||
[&](const SortItem& a, const SortItem& b) {
|
||||
int cmp;
|
||||
if (spec.ColumnUserID == PATCH_COLUMN_STATUS) {
|
||||
// sort by displayed status (matches the checkbox), then
|
||||
// by name as a tiebreaker
|
||||
const bool a_on = a->last_status == patcher::PatchStatus::Enabled;
|
||||
const bool b_on = b->last_status == patcher::PatchStatus::Enabled;
|
||||
if (a_on != b_on) {
|
||||
cmp = a_on ? -1 : 1;
|
||||
if (a.enabled != b.enabled) {
|
||||
cmp = a.enabled ? -1 : 1;
|
||||
} 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 {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
sort_specs->SpecsDirty = false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user