Files
spice2x.github.io/src/spice2x/patcher/loader_group.cpp
T
bicarus be4bb00390 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?*
2026-07-19 02:10:13 -07:00

180 lines
6.8 KiB
C++

#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();
}
}
}