mirror of
https://github.com/RPCS3/rpcs3.git
synced 2025-07-03 21:41:26 +12:00
patch manager
This commit is contained in:
parent
1668900fb8
commit
2dca8d84e1
14 changed files with 1318 additions and 190 deletions
|
@ -1,10 +1,27 @@
|
|||
#include "bin_patch.h"
|
||||
#include "util/yaml.hpp"
|
||||
#include "bin_patch.h"
|
||||
#include "File.h"
|
||||
#include "Config.h"
|
||||
|
||||
LOG_CHANNEL(patch_log);
|
||||
|
||||
template <>
|
||||
void fmt_class_string<YAML::NodeType::value>::format(std::string& out, u64 arg)
|
||||
{
|
||||
format_enum(out, arg, [](YAML::NodeType::value value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case YAML::NodeType::Undefined: return "Undefined";
|
||||
case YAML::NodeType::Null: return "Null";
|
||||
case YAML::NodeType::Scalar: return "Scalar";
|
||||
case YAML::NodeType::Sequence: return "Sequence";
|
||||
case YAML::NodeType::Map: return "Map";
|
||||
}
|
||||
|
||||
return unknown;
|
||||
});
|
||||
}
|
||||
|
||||
template <>
|
||||
void fmt_class_string<patch_type>::format(std::string& out, u64 arg)
|
||||
{
|
||||
|
@ -30,219 +47,507 @@ void fmt_class_string<patch_type>::format(std::string& out, u64 arg)
|
|||
});
|
||||
}
|
||||
|
||||
void patch_engine::append(const std::string& patch)
|
||||
patch_engine::patch_engine()
|
||||
{
|
||||
if (fs::file f{patch})
|
||||
{
|
||||
auto [root, error] = yaml_load(f.to_string());
|
||||
const std::string patches_path = fs::get_config_dir() + "patches/";
|
||||
|
||||
if (!error.empty())
|
||||
if (!fs::create_path(patches_path))
|
||||
{
|
||||
patch_log.fatal("Failed to create path: %s (%s)", patches_path, fs::g_tls_error);
|
||||
}
|
||||
}
|
||||
|
||||
std::string patch_engine::get_patch_config_path()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
return fs::get_config_dir() + "config/patch_config.yml";
|
||||
#else
|
||||
return fs::get_config_dir() + "patch_config.yml";
|
||||
#endif
|
||||
}
|
||||
|
||||
void patch_engine::load(patch_map& patches_map, const std::string& path)
|
||||
{
|
||||
// Load patch file
|
||||
fs::file file{ path };
|
||||
|
||||
if (!file)
|
||||
{
|
||||
// Do nothing
|
||||
return;
|
||||
}
|
||||
|
||||
// Interpret yaml nodes
|
||||
auto [root, error] = yaml_load(file.to_string());
|
||||
|
||||
if (!error.empty())
|
||||
{
|
||||
patch_log.fatal("Failed to load patch file %s:\n%s", path, error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Load patch config to determine which patches are enabled
|
||||
patch_config_map patch_config = load_config();
|
||||
|
||||
static const std::string target_version = "1.0";
|
||||
std::string version;
|
||||
bool is_legacy_patch = false;
|
||||
|
||||
if (const auto version_node = root["Version"])
|
||||
{
|
||||
version = version_node.Scalar();
|
||||
|
||||
if (version != target_version)
|
||||
{
|
||||
patch_log.fatal("Failed to load patch file %s:\n%s", patch, error);
|
||||
patch_log.error("Patch engine target version %s does not match file version %s in %s", target_version, version, path);
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto pair : root)
|
||||
// We don't need the Version node in local memory anymore
|
||||
root.remove("Version");
|
||||
}
|
||||
else
|
||||
{
|
||||
patch_log.warning("Patch engine version %s: Reading legacy patch file %s", target_version, path);
|
||||
is_legacy_patch = true;
|
||||
}
|
||||
|
||||
// Go through each main key in the file
|
||||
for (auto pair : root)
|
||||
{
|
||||
const auto& main_key = pair.first.Scalar();
|
||||
|
||||
// Use old logic and yaml layout if this is a legacy patch
|
||||
if (is_legacy_patch)
|
||||
{
|
||||
auto& name = pair.first.Scalar();
|
||||
auto& data = m_map[name];
|
||||
struct patch_info info{};
|
||||
info.hash = main_key;
|
||||
info.enabled = true;
|
||||
info.is_legacy = true;
|
||||
|
||||
for (auto patch : pair.second)
|
||||
read_patch_node(info, pair.second, root);
|
||||
|
||||
// Find or create an entry matching the key/hash in our map
|
||||
auto& title_info = patches_map[main_key];
|
||||
title_info.hash = main_key;
|
||||
title_info.is_legacy = true;
|
||||
title_info.patch_info_map["legacy"] = info;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use new logic and yaml layout
|
||||
|
||||
if (const auto yml_type = pair.second.Type(); yml_type != YAML::NodeType::Map)
|
||||
{
|
||||
patch_log.error("Skipping key %s: expected Map, found %s (file: %s)", main_key, yml_type, path);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip Anchors
|
||||
if (main_key == "Anchors")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string title;
|
||||
std::string serials;
|
||||
|
||||
if (const auto title_node = pair.second["Title"])
|
||||
{
|
||||
title = title_node.Scalar();
|
||||
}
|
||||
|
||||
if (const auto serials_node = pair.second["Serials"])
|
||||
{
|
||||
serials = serials_node.Scalar();
|
||||
}
|
||||
|
||||
if (const auto patches_node = pair.second["Patches"])
|
||||
{
|
||||
if (const auto yml_type = patches_node.Type(); yml_type != YAML::NodeType::Map)
|
||||
{
|
||||
u64 type64 = 0;
|
||||
cfg::try_to_enum_value(&type64, &fmt_class_string<patch_type>::format, patch[0].Scalar());
|
||||
patch_log.error("Skipping Patches: expected Map, found %s (key: %s, file: %s)", yml_type, main_key, path);
|
||||
continue;
|
||||
}
|
||||
|
||||
struct patch info{};
|
||||
info.type = static_cast<patch_type>(type64);
|
||||
info.offset = patch[1].as<u32>(0);
|
||||
// Find or create an entry matching the key/hash in our map
|
||||
auto& title_info = patches_map[main_key];
|
||||
title_info.is_legacy = false;
|
||||
title_info.hash = main_key;
|
||||
title_info.title = title;
|
||||
title_info.serials = serials;
|
||||
title_info.version = version;
|
||||
|
||||
switch (info.type)
|
||||
// Go through each patch
|
||||
for (auto patches_entry : patches_node)
|
||||
{
|
||||
// Each key in "Patches" is also the patch description
|
||||
const std::string description = patches_entry.first.Scalar();
|
||||
|
||||
// Find out if this patch was enabled in the patch config
|
||||
const bool enabled = patch_config[main_key][description];
|
||||
|
||||
// Compile patch information
|
||||
|
||||
if (const auto yml_type = patches_entry.second.Type(); yml_type != YAML::NodeType::Map)
|
||||
{
|
||||
case patch_type::load:
|
||||
{
|
||||
// Special syntax: copy named sequence (must be loaded before)
|
||||
const auto found = m_map.find(patch[1].Scalar());
|
||||
|
||||
if (found != m_map.end())
|
||||
{
|
||||
// Address modifier (optional)
|
||||
const u32 mod = patch[2].as<u32>(0);
|
||||
|
||||
for (const auto& rd : found->second)
|
||||
{
|
||||
info = rd;
|
||||
info.offset += mod;
|
||||
data.emplace_back(info);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: error
|
||||
break;
|
||||
}
|
||||
case patch_type::bef32:
|
||||
case patch_type::lef32:
|
||||
{
|
||||
info.value = std::bit_cast<u32>(patch[2].as<f32>());
|
||||
break;
|
||||
}
|
||||
case patch_type::bef64:
|
||||
case patch_type::lef64:
|
||||
{
|
||||
info.value = std::bit_cast<u64>(patch[2].as<f64>());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
info.value = patch[2].as<u64>();
|
||||
break;
|
||||
}
|
||||
patch_log.error("Skipping Patch key %s: expected Map, found %s (key: %s, file: %s)", description, yml_type, main_key, path);
|
||||
continue;
|
||||
}
|
||||
|
||||
data.emplace_back(info);
|
||||
struct patch_info info {};
|
||||
info.enabled = enabled;
|
||||
info.description = description;
|
||||
info.hash = main_key;
|
||||
info.version = version;
|
||||
info.serials = serials;
|
||||
info.title = title;
|
||||
|
||||
if (const auto author_node = patches_entry.second["Author"])
|
||||
{
|
||||
info.author = author_node.Scalar();
|
||||
}
|
||||
|
||||
if (const auto patch_version_node = patches_entry.second["Version"])
|
||||
{
|
||||
info.patch_version = patch_version_node.Scalar();
|
||||
}
|
||||
|
||||
if (const auto notes_node = patches_entry.second["Notes"])
|
||||
{
|
||||
info.notes = notes_node.Scalar();
|
||||
}
|
||||
|
||||
if (const auto patch_node = patches_entry.second["Patch"])
|
||||
{
|
||||
read_patch_node(info, patch_node, root);
|
||||
}
|
||||
|
||||
// Insert patch information
|
||||
title_info.patch_info_map[description] = info;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
patch_type patch_engine::get_patch_type(YAML::Node node)
|
||||
{
|
||||
u64 type_val = 0;
|
||||
cfg::try_to_enum_value(&type_val, &fmt_class_string<patch_type>::format, node.Scalar());
|
||||
return static_cast<patch_type>(type_val);
|
||||
}
|
||||
|
||||
void patch_engine::add_patch_data(YAML::Node node, patch_info& info, u32 modifier, const YAML::Node& root)
|
||||
{
|
||||
const auto type_node = node[0];
|
||||
auto addr_node = node[1];
|
||||
const auto value_node = node[2];
|
||||
|
||||
struct patch_data p_data{};
|
||||
p_data.type = get_patch_type(type_node);
|
||||
p_data.offset = addr_node.as<u32>(0) + modifier;
|
||||
|
||||
switch (p_data.type)
|
||||
{
|
||||
case patch_type::load:
|
||||
{
|
||||
// Special syntax: anchors (named sequence)
|
||||
|
||||
// Most legacy patches don't use the anchor syntax correctly, so try to sanitize it.
|
||||
if (info.is_legacy)
|
||||
{
|
||||
if (const auto yml_type = addr_node.Type(); yml_type == YAML::NodeType::Scalar)
|
||||
{
|
||||
const auto anchor = addr_node.Scalar();
|
||||
patch_log.warning("Incorrect anchor syntax found in legacy patch: %s (key: %s)", anchor, info.hash);
|
||||
|
||||
if (!(addr_node = root[anchor]))
|
||||
{
|
||||
patch_log.error("Anchor not found in legacy patch: %s (key: %s)", anchor, info.hash);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the anchor was resolved.
|
||||
if (const auto yml_type = addr_node.Type(); yml_type != YAML::NodeType::Sequence)
|
||||
{
|
||||
patch_log.error("Skipping sequence: expected Sequence, found %s (key: %s)", yml_type, info.hash);
|
||||
return;
|
||||
}
|
||||
|
||||
// Address modifier (optional)
|
||||
const u32 mod = value_node.as<u32>(0);
|
||||
|
||||
for (const auto& item : addr_node)
|
||||
{
|
||||
add_patch_data(item, info, mod, root);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
case patch_type::bef32:
|
||||
case patch_type::lef32:
|
||||
{
|
||||
p_data.value = std::bit_cast<u32>(value_node.as<f32>());
|
||||
break;
|
||||
}
|
||||
case patch_type::bef64:
|
||||
case patch_type::lef64:
|
||||
{
|
||||
p_data.value = std::bit_cast<u64>(value_node.as<f64>());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
p_data.value = value_node.as<u64>();
|
||||
}
|
||||
}
|
||||
|
||||
info.data_list.emplace_back(p_data);
|
||||
}
|
||||
|
||||
void patch_engine::read_patch_node(patch_info& info, YAML::Node node, const YAML::Node& root)
|
||||
{
|
||||
if (const auto yml_type = node.Type(); yml_type != YAML::NodeType::Sequence)
|
||||
{
|
||||
patch_log.error("Skipping patch node %s: expected Sequence, found %s (key: %s)", info.description, yml_type, info.hash);
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto patch : node)
|
||||
{
|
||||
add_patch_data(patch, info, 0, root);
|
||||
}
|
||||
}
|
||||
|
||||
void patch_engine::append(const std::string& patch)
|
||||
{
|
||||
load(m_map, patch);
|
||||
}
|
||||
|
||||
void patch_engine::append_global_patches()
|
||||
{
|
||||
// Legacy patch.yml
|
||||
load(m_map, fs::get_config_dir() + "patch.yml");
|
||||
|
||||
// New patch.yml
|
||||
load(m_map, fs::get_config_dir() + "patches/patch.yml");
|
||||
}
|
||||
|
||||
void patch_engine::append_title_patches(const std::string& title_id)
|
||||
{
|
||||
if (title_id.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Legacy patch.yml
|
||||
load(m_map, fs::get_config_dir() + "data/" + title_id + "/patch.yml");
|
||||
|
||||
// New patch.yml
|
||||
load(m_map, fs::get_config_dir() + "patches/" + title_id + "_patch.yml");
|
||||
}
|
||||
|
||||
std::size_t patch_engine::apply(const std::string& name, u8* dst) const
|
||||
{
|
||||
const auto found = m_map.find(name);
|
||||
|
||||
if (found == m_map.cend())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Apply modifications sequentially
|
||||
for (const auto& p : found->second)
|
||||
{
|
||||
auto ptr = dst + p.offset;
|
||||
|
||||
switch (p.type)
|
||||
{
|
||||
case patch_type::load:
|
||||
{
|
||||
// Invalid in this context
|
||||
break;
|
||||
}
|
||||
case patch_type::byte:
|
||||
{
|
||||
*ptr = static_cast<u8>(p.value);
|
||||
break;
|
||||
}
|
||||
case patch_type::le16:
|
||||
{
|
||||
*reinterpret_cast<le_t<u16, 1>*>(ptr) = static_cast<u16>(p.value);
|
||||
break;
|
||||
}
|
||||
case patch_type::le32:
|
||||
case patch_type::lef32:
|
||||
{
|
||||
*reinterpret_cast<le_t<u32, 1>*>(ptr) = static_cast<u32>(p.value);
|
||||
break;
|
||||
}
|
||||
case patch_type::le64:
|
||||
case patch_type::lef64:
|
||||
{
|
||||
*reinterpret_cast<le_t<u64, 1>*>(ptr) = static_cast<u64>(p.value);
|
||||
break;
|
||||
}
|
||||
case patch_type::be16:
|
||||
{
|
||||
*reinterpret_cast<be_t<u16, 1>*>(ptr) = static_cast<u16>(p.value);
|
||||
break;
|
||||
}
|
||||
case patch_type::be32:
|
||||
case patch_type::bef32:
|
||||
{
|
||||
*reinterpret_cast<be_t<u32, 1>*>(ptr) = static_cast<u32>(p.value);
|
||||
break;
|
||||
}
|
||||
case patch_type::be64:
|
||||
case patch_type::bef64:
|
||||
{
|
||||
*reinterpret_cast<be_t<u64, 1>*>(ptr) = static_cast<u64>(p.value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return found->second.size();
|
||||
return apply_patch<false>(name, dst, 0, 0);
|
||||
}
|
||||
|
||||
std::size_t patch_engine::apply_with_ls_check(const std::string& name, u8* dst, u32 filesz, u32 ls_addr) const
|
||||
{
|
||||
u32 rejected = 0;
|
||||
return apply_patch<true>(name, dst, filesz, ls_addr);
|
||||
}
|
||||
|
||||
const auto found = m_map.find(name);
|
||||
|
||||
if (found == m_map.cend())
|
||||
template <bool check_local_storage>
|
||||
std::size_t patch_engine::apply_patch(const std::string& name, u8* dst, u32 filesz, u32 ls_addr) const
|
||||
{
|
||||
if (m_map.find(name) == m_map.cend())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Apply modifications sequentially
|
||||
for (const auto& p : found->second)
|
||||
{
|
||||
auto ptr = dst + (p.offset - ls_addr);
|
||||
size_t applied_total = 0;
|
||||
const auto& title_info = m_map.at(name);
|
||||
|
||||
if(p.offset < ls_addr || p.offset >= (ls_addr + filesz))
|
||||
// Apply modifications sequentially
|
||||
for (const auto& [description, patch] : title_info.patch_info_map)
|
||||
{
|
||||
if (!patch.enabled)
|
||||
{
|
||||
// This patch is out of range for this segment
|
||||
rejected++;
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (p.type)
|
||||
size_t applied = 0;
|
||||
|
||||
for (const auto& p : patch.data_list)
|
||||
{
|
||||
case patch_type::load:
|
||||
{
|
||||
// Invalid in this context
|
||||
break;
|
||||
u32 offset = p.offset;
|
||||
|
||||
if constexpr (check_local_storage)
|
||||
{
|
||||
offset -= ls_addr;
|
||||
|
||||
if (offset < ls_addr || offset >= (ls_addr + filesz))
|
||||
{
|
||||
// This patch is out of range for this segment
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
auto ptr = dst + offset;
|
||||
|
||||
switch (p.type)
|
||||
{
|
||||
case patch_type::load:
|
||||
{
|
||||
// Invalid in this context
|
||||
continue;
|
||||
}
|
||||
case patch_type::byte:
|
||||
{
|
||||
*ptr = static_cast<u8>(p.value);
|
||||
break;
|
||||
}
|
||||
case patch_type::le16:
|
||||
{
|
||||
*reinterpret_cast<le_t<u16, 1>*>(ptr) = static_cast<u16>(p.value);
|
||||
break;
|
||||
}
|
||||
case patch_type::le32:
|
||||
case patch_type::lef32:
|
||||
{
|
||||
*reinterpret_cast<le_t<u32, 1>*>(ptr) = static_cast<u32>(p.value);
|
||||
break;
|
||||
}
|
||||
case patch_type::le64:
|
||||
case patch_type::lef64:
|
||||
{
|
||||
*reinterpret_cast<le_t<u64, 1>*>(ptr) = static_cast<u64>(p.value);
|
||||
break;
|
||||
}
|
||||
case patch_type::be16:
|
||||
{
|
||||
*reinterpret_cast<be_t<u16, 1>*>(ptr) = static_cast<u16>(p.value);
|
||||
break;
|
||||
}
|
||||
case patch_type::be32:
|
||||
case patch_type::bef32:
|
||||
{
|
||||
*reinterpret_cast<be_t<u32, 1>*>(ptr) = static_cast<u32>(p.value);
|
||||
break;
|
||||
}
|
||||
case patch_type::be64:
|
||||
case patch_type::bef64:
|
||||
{
|
||||
*reinterpret_cast<be_t<u64, 1>*>(ptr) = static_cast<u64>(p.value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
++applied;
|
||||
}
|
||||
case patch_type::byte:
|
||||
|
||||
if (title_info.is_legacy)
|
||||
{
|
||||
*ptr = static_cast<u8>(p.value);
|
||||
break;
|
||||
patch_log.notice("Applied legacy patch (<- %d)", applied);
|
||||
}
|
||||
case patch_type::le16:
|
||||
else
|
||||
{
|
||||
*reinterpret_cast<le_t<u16, 1>*>(ptr) = static_cast<u16>(p.value);
|
||||
break;
|
||||
patch_log.notice("Applied patch (description='%s', author='%s', patch_version='%s', file_version='%s') (<- %d)", description, patch.author, patch.patch_version, patch.version, applied);
|
||||
}
|
||||
case patch_type::le32:
|
||||
case patch_type::lef32:
|
||||
|
||||
applied_total += applied;
|
||||
}
|
||||
|
||||
return applied_total;
|
||||
}
|
||||
|
||||
void patch_engine::save_config(const patch_map& patches_map)
|
||||
{
|
||||
const std::string path = get_patch_config_path();
|
||||
patch_log.notice("Saving patch config file %s", path);
|
||||
|
||||
fs::file file(path, fs::rewrite);
|
||||
if (!file)
|
||||
{
|
||||
patch_log.fatal("Failed to open patch config file %s", path);
|
||||
return;
|
||||
}
|
||||
|
||||
YAML::Emitter out;
|
||||
out << YAML::BeginMap;
|
||||
|
||||
patch_config_map config_map;
|
||||
|
||||
for (const auto& [hash, title_info] : patches_map)
|
||||
{
|
||||
if (title_info.is_legacy)
|
||||
{
|
||||
*reinterpret_cast<le_t<u32, 1>*>(ptr) = static_cast<u32>(p.value);
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
case patch_type::le64:
|
||||
case patch_type::lef64:
|
||||
|
||||
for (const auto& [description, patch] : title_info.patch_info_map)
|
||||
{
|
||||
*reinterpret_cast<le_t<u64, 1>*>(ptr) = static_cast<u64>(p.value);
|
||||
break;
|
||||
config_map[hash][description] = patch.enabled;
|
||||
}
|
||||
case patch_type::be16:
|
||||
|
||||
if (config_map[hash].size() > 0)
|
||||
{
|
||||
*reinterpret_cast<be_t<u16, 1>*>(ptr) = static_cast<u16>(p.value);
|
||||
break;
|
||||
out << hash;
|
||||
out << YAML::BeginMap;
|
||||
|
||||
for (const auto& [description, enabled] : config_map[hash])
|
||||
{
|
||||
out << description;
|
||||
out << enabled;
|
||||
}
|
||||
|
||||
out << YAML::EndMap;
|
||||
}
|
||||
case patch_type::be32:
|
||||
case patch_type::bef32:
|
||||
}
|
||||
out << YAML::EndMap;
|
||||
|
||||
file.write(out.c_str(), out.size());
|
||||
}
|
||||
|
||||
patch_engine::patch_config_map patch_engine::load_config()
|
||||
{
|
||||
patch_config_map config_map;
|
||||
|
||||
const std::string path = get_patch_config_path();
|
||||
patch_log.notice("Loading patch config file %s", path);
|
||||
|
||||
if (fs::file f{ path })
|
||||
{
|
||||
auto [root, error] = yaml_load(f.to_string());
|
||||
|
||||
if (!error.empty())
|
||||
{
|
||||
*reinterpret_cast<be_t<u32, 1>*>(ptr) = static_cast<u32>(p.value);
|
||||
break;
|
||||
patch_log.fatal("Failed to load patch config file %s:\n%s", path, error);
|
||||
return config_map;
|
||||
}
|
||||
case patch_type::be64:
|
||||
case patch_type::bef64:
|
||||
|
||||
for (auto pair : root)
|
||||
{
|
||||
*reinterpret_cast<be_t<u64, 1>*>(ptr) = static_cast<u64>(p.value);
|
||||
break;
|
||||
}
|
||||
auto& hash = pair.first.Scalar();
|
||||
auto& data = config_map[hash];
|
||||
|
||||
if (const auto yml_type = pair.second.Type(); yml_type != YAML::NodeType::Map)
|
||||
{
|
||||
patch_log.error("Error loading patch config key %s: expected Map, found %s (file: %s)", hash, yml_type, path);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (auto patch : pair.second)
|
||||
{
|
||||
const auto description = patch.first.Scalar();
|
||||
const auto enabled = patch.second.as<bool>(false);
|
||||
|
||||
data[description] = enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (found->second.size() - rejected);
|
||||
return config_map;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue