#include "TaskData.h" #include #include #include "Resource/GeneralConfiger.h" #include "Resource/TemplResource.h" #include "Utils/AsstRanges.hpp" #include "Utils/AsstTypes.h" #include "Utils/Logger.hpp" #include "Utils/StringMisc.hpp" const std::unordered_set& asst::TaskData::get_templ_required() const noexcept { return m_templ_required; } bool asst::TaskData::parse(const json::value& json) { LogTraceFunction; const auto& json_obj = json.as_object(); { std::unordered_map to_be_generated; for (const std::string& name : json_obj | views::keys) { to_be_generated[name] = true; } auto generate_task_and_its_base = [&](const std::string& name) -> bool { auto generate_task = [&](const std::string& name, std::string_view prefix, std::shared_ptr base_ptr, const json::value& task_json) { auto task_info_ptr = generate_task_info(name, task_json, base_ptr, prefix); if (task_info_ptr == nullptr) { return false; } to_be_generated[name] = false; m_all_tasks_info[name] = task_info_ptr; return true; }; std::function generate_fun; // must_true 若为真,那么 return false 了就是炸了。 // 否则可能只是某个 B@A 的任务没定义 A(这不是少见现象,例如 Roguelike@Abandon) generate_fun = [&](const std::string& name, bool must_true) -> bool { if (!to_be_generated[name]) { // 已生成(它是之前加载过的某个资源的 base) if (m_all_tasks_info.contains(name)) { return true; } // 不在 json 内且未生成(例如生成 C@B@A 时没有定义 B@A,而是定义了 A) if (size_t p = name.find('@'); p != std::string::npos) { return generate_fun(name.substr(p + 1), must_true); } // 例如 Roguelike@Abandon 在这里如果 must_true = true,会报 `Unknown task: Abandon` if (must_true) { Log.error("Unknown task:", name); } return false; } const json::value& task_json = json_obj.at(name); if (auto opt = task_json.find("baseTask")) { std::string base = opt.value(); return generate_fun(base, must_true) && generate_task(name, "", get(base, false), task_json); } if (size_t p = name.find('@'); p != std::string::npos) { if (std::string base = name.substr(p + 1); generate_fun(base, false)) { return generate_task(name, name.substr(0, p), get(base, false), task_json); } // 这类任务有点多,非必要不输出( // Log.debug("Task", name, "based on unknown task, just use task_json."); } return generate_task(name, "", nullptr, task_json); }; return generate_fun(name, true); }; for (const std::string& name : json_obj | views::keys) { generate_task_and_its_base(name); } } #ifdef ASST_DEBUG { bool validity = true; // 语法检查 for (const auto& [name, task_json] : json_obj) { validity &= syntax_check(name, task_json); } std::unordered_map> dependency_graph; // "#" 型任务依赖关系 (有向图邻接表) std::unordered_map checked; // 拓扑排序相关的标志 static const std::unordered_set accepted_type = { "next", "sub", "on_error_next", "exceeded_next", "reduce_other_times", }; for (const auto& [name, task] : m_all_tasks_info) { auto check_and_link = [&](const std::vector& task_list, std::string node_name) { for (const auto& task_name : task_list) { size_t pos = task_name.find('#'); // next、sub 等的存在性检查 if (pos == std::string::npos) { if (get(task_name, false) == nullptr) { Log.error(node_name, task_name, "is null"); validity = false; } continue; } std::string other_task_name = task_name.substr(0, pos); if (get(other_task_name, false) == nullptr) { Log.error(node_name, task_name, "is null"); validity = false; } std::string type = task_name.substr(pos + 1); if (!accepted_type.contains(type)) { Log.error(node_name, task_name, "has unknown type:", type); validity = false; } else { // 建立一条依赖关系 (有向边) dependency_graph[node_name].emplace_back(task_name); } } }; check_and_link(task->next, name + "#next"); check_and_link(task->sub, name + "#sub"); check_and_link(task->exceeded_next, name + "#exceeded_next"); check_and_link(task->on_error_next, name + "#on_error_next"); check_and_link(task->reduce_other_times, name + "#reduce_other_times"); } // dfs 检查 "#" 型任务是否循环依赖 (有向无环图) auto check_circle = [&](const std::string& x) { std::function dfs; dfs = [&](const std::string& x) { checked[x] = -1; for (const auto& y : dependency_graph[x]) { if (checked[y] == 0) { if (!dfs(y)) [[unlikely]] { return false; } } else if (checked[y] < 0) [[unlikely]] { Log.error("Task", y, "has circular dependency."); return false; } } checked[x] = 1; return true; }; return dfs(x); }; for (const auto& name : dependency_graph | views::keys) { if (!checked[name] && !check_circle(name)) { validity = false; break; } } if (!validity) return false; } #endif return true; } std::shared_ptr asst::TaskData::generate_task_info(const std::string& name, const json::value& task_json, std::shared_ptr default_ptr, std::string_view task_prefix) { if (default_ptr == nullptr) { default_ptr = default_task_info_ptr; task_prefix = ""; } // 获取 algorithm 并按照 algorithm 生成 TaskInfo auto algorithm = default_ptr->algorithm; std::shared_ptr default_derived_ptr = default_ptr; if (auto opt = task_json.find("algorithm")) { std::string algorithm_str = opt.value(); utils::tolowers(algorithm_str); algorithm = get_algorithm_type(algorithm_str); if (default_ptr->algorithm != algorithm) { // 相同 algorithm 时才继承派生类成员 default_derived_ptr = nullptr; } } std::shared_ptr task_info_ptr = nullptr; switch (algorithm) { case AlgorithmType::MatchTemplate: task_info_ptr = generate_match_task_info(name, task_json, std::dynamic_pointer_cast(default_derived_ptr)); break; case AlgorithmType::OcrDetect: task_info_ptr = generate_ocr_task_info(name, task_json, std::dynamic_pointer_cast(default_derived_ptr)); break; case AlgorithmType::Hash: task_info_ptr = generate_hash_task_info(name, task_json, std::dynamic_pointer_cast(default_derived_ptr)); break; case AlgorithmType::JustReturn: task_info_ptr = std::make_shared(); break; default: Log.error("Unknown algorithm in task", name); return nullptr; } // 不管什么algorithm,都有基础成员(next, roi, 等等) if (!append_base_task_info(task_info_ptr, name, task_json, default_ptr, task_prefix)) { return nullptr; } task_info_ptr->algorithm = algorithm; task_info_ptr->name = name; return task_info_ptr; } std::shared_ptr asst::TaskData::generate_match_task_info(const std::string& name, const json::value& task_json, std::shared_ptr default_ptr) { if (default_ptr == nullptr) { default_ptr = default_match_task_info_ptr; } auto match_task_info_ptr = std::make_shared(); // template 留空时不从模板任务继承 match_task_info_ptr->templ_name = task_json.get("template", name + ".png"); m_templ_required.emplace(match_task_info_ptr->templ_name); // 其余若留空则继承模板任务 match_task_info_ptr->templ_threshold = task_json.get("templThreshold", default_ptr->templ_threshold); match_task_info_ptr->special_threshold = task_json.get("specialThreshold", default_ptr->special_threshold); if (auto opt = task_json.find("maskRange")) { auto& mask_range = *opt; match_task_info_ptr->mask_range = std::make_pair(static_cast(mask_range[0]), static_cast(mask_range[1])); } else { match_task_info_ptr->mask_range = default_ptr->mask_range; } return match_task_info_ptr; } std::shared_ptr asst::TaskData::generate_ocr_task_info([[maybe_unused]] const std::string& name, const json::value& task_json, std::shared_ptr default_ptr) { if (default_ptr == nullptr) { default_ptr = default_ocr_task_info_ptr; } auto ocr_task_info_ptr = std::make_shared(); if (auto opt = task_json.find("text")) { for (const json::value& text : opt.value()) { ocr_task_info_ptr->text.emplace_back(text.as_string()); } } else { #ifdef ASST_DEBUG if (default_ptr->text.empty()) { Log.warn("Ocr task", name, "has implicit empty text."); } #endif ocr_task_info_ptr->text = default_ptr->text; } ocr_task_info_ptr->full_match = task_json.get("fullMatch", default_ptr->full_match); if (auto opt = task_json.find("ocrReplace")) { for (const json::value& rep : opt.value()) { ocr_task_info_ptr->replace_map.emplace(rep[0].as_string(), rep[1].as_string()); } } else { ocr_task_info_ptr->replace_map = default_ptr->replace_map; } return ocr_task_info_ptr; } std::shared_ptr asst::TaskData::generate_hash_task_info([[maybe_unused]] const std::string& name, const json::value& task_json, std::shared_ptr default_ptr) { if (default_ptr == nullptr) { default_ptr = default_hash_task_info_ptr; } auto hash_task_info_ptr = std::make_shared(); if (auto opt = task_json.find("hash")) { for (const json::value& hash : opt.value()) { hash_task_info_ptr->hashes.emplace_back(hash.as_string()); } } else { #ifdef ASST_DEBUG if (default_ptr->hashes.empty()) { Log.warn("Hash task", name, "has implicit empty hashes."); } #endif hash_task_info_ptr->hashes = default_ptr->hashes; } hash_task_info_ptr->dist_threshold = task_json.get("threshold", default_ptr->dist_threshold); if (auto opt = task_json.find("maskRange")) { auto& mask_range = *opt; hash_task_info_ptr->mask_range = std::make_pair(static_cast(mask_range[0]), static_cast(mask_range[1])); } else { hash_task_info_ptr->mask_range = default_ptr->mask_range; } hash_task_info_ptr->bound = task_json.get("bound", default_ptr->bound); return hash_task_info_ptr; } bool asst::TaskData::append_base_task_info(std::shared_ptr task_info_ptr, const std::string& name, const json::value& task_json, std::shared_ptr default_ptr, std::string_view task_prefix) { if (default_ptr == nullptr) { default_ptr = default_task_info_ptr; } if (auto opt = task_json.find("action")) { std::string action = opt.value(); utils::tolowers(action); task_info_ptr->action = get_action_type(action); if (task_info_ptr->action == ProcessTaskAction::Invalid) [[unlikely]] { Log.error("Unknown action:", action, ", Task:", name); return false; } } else { task_info_ptr->action = default_ptr->action; } task_info_ptr->cache = task_json.get("cache", default_ptr->cache); task_info_ptr->max_times = task_json.get("maxTimes", default_ptr->max_times); if (auto opt = task_json.find("exceededNext")) { for (const json::value& exceed_next : opt.value()) { task_info_ptr->exceeded_next.emplace_back(exceed_next.as_string()); } } else { task_info_ptr->exceeded_next = append_prefix(default_ptr->exceeded_next, task_prefix); } if (auto opt = task_json.find("onErrorNext")) { for (const json::value& on_error_next : opt.value()) { task_info_ptr->on_error_next.emplace_back(on_error_next.as_string()); } } else { task_info_ptr->on_error_next = append_prefix(default_ptr->on_error_next, task_prefix); } task_info_ptr->pre_delay = task_json.get("preDelay", default_ptr->pre_delay); task_info_ptr->rear_delay = task_json.get("rearDelay", default_ptr->rear_delay); if (auto opt = task_json.find("reduceOtherTimes")) { for (const json::value& reduce : opt.value()) { task_info_ptr->reduce_other_times.emplace_back(reduce.as_string()); } } else { task_info_ptr->reduce_other_times = append_prefix(default_ptr->reduce_other_times, task_prefix); } if (auto opt = task_json.find("roi")) { auto& roi_arr = *opt; int x = static_cast(roi_arr[0]); int y = static_cast(roi_arr[1]); int width = static_cast(roi_arr[2]); int height = static_cast(roi_arr[3]); #ifdef ASST_DEBUG if (x + width > WindowWidthDefault || y + height > WindowHeightDefault) { Log.error(name, "roi is out of bounds"); return false; } #endif task_info_ptr->roi = Rect(x, y, width, height); } else { task_info_ptr->roi = default_ptr->roi; } if (auto opt = task_json.find("sub")) { for (const json::value& sub : opt.value()) { task_info_ptr->sub.emplace_back(sub.as_string()); } } else { task_info_ptr->sub = append_prefix(default_ptr->sub, task_prefix); } task_info_ptr->sub_error_ignored = task_json.get("subErrorIgnored", default_ptr->sub_error_ignored); if (auto opt = task_json.find("next")) { for (const json::value& next : opt.value()) { task_info_ptr->next.emplace_back(next.as_string()); } } else { task_info_ptr->next = append_prefix(default_ptr->next, task_prefix); } if (auto opt = task_json.find("rectMove")) { auto& move_arr = opt.value(); task_info_ptr->rect_move = Rect(move_arr[0].as_integer(), move_arr[1].as_integer(), move_arr[2].as_integer(), move_arr[3].as_integer()); } else { task_info_ptr->rect_move = default_ptr->rect_move; } if (auto opt = task_json.find("specificRect")) { auto& rect_arr = opt.value(); task_info_ptr->specific_rect = Rect(rect_arr[0].as_integer(), rect_arr[1].as_integer(), rect_arr[2].as_integer(), rect_arr[3].as_integer()); } else { task_info_ptr->specific_rect = default_ptr->specific_rect; } return true; } std::shared_ptr asst::TaskData::_default_match_task_info() { auto match_task_info_ptr = std::make_shared(); match_task_info_ptr->templ_name = "__INVALID__"; match_task_info_ptr->templ_threshold = TemplThresholdDefault; match_task_info_ptr->special_threshold = 0; return match_task_info_ptr; } std::shared_ptr asst::TaskData::_default_ocr_task_info() { auto ocr_task_info_ptr = std::make_shared(); ocr_task_info_ptr->full_match = false; return ocr_task_info_ptr; } std::shared_ptr asst::TaskData::_default_hash_task_info() { auto hash_task_info_ptr = std::make_shared(); hash_task_info_ptr->dist_threshold = 0; hash_task_info_ptr->bound = true; return hash_task_info_ptr; } std::shared_ptr asst::TaskData::_default_task_info() { auto task_info_ptr = std::make_shared(); task_info_ptr->algorithm = AlgorithmType::MatchTemplate; task_info_ptr->action = ProcessTaskAction::DoNothing; task_info_ptr->cache = true; task_info_ptr->max_times = INT_MAX; task_info_ptr->pre_delay = 0; task_info_ptr->rear_delay = 0; task_info_ptr->roi = Rect(); task_info_ptr->sub_error_ignored = false; task_info_ptr->rect_move = Rect(); task_info_ptr->specific_rect = Rect(); return task_info_ptr; } #ifdef ASST_DEBUG // 为了解决类似 beddc7c828126c678391e0b4da288db6d2c2d58a 导致的问题,加载的时候做一个语法检查 // 主要是处理是否包含未知键值的问题 bool asst::TaskData::syntax_check(const std::string& task_name, const json::value& task_json) { static const std::unordered_map> allowed_key_under_algorithm = { { AlgorithmType::Invalid, { "algorithm", "baseTask", "template", "text", "action", "sub", "subErrorIgnored", "next", "maxTimes", "exceededNext", "onErrorNext", "preDelay", "rearDelay", "roi", "cache", "rectMove", "reduceOtherTimes", "templThreshold", "maskRange", "fullMatch", "ocrReplace", "hash", "specialThreshold", "threshold", } }, { AlgorithmType::MatchTemplate, { "algorithm", "baseTask", "template", "action", "sub", "subErrorIgnored", "next", "maxTimes", "exceededNext", "onErrorNext", "preDelay", "rearDelay", "roi", "cache", "rectMove", "reduceOtherTimes", "templThreshold", "maskRange", } }, { AlgorithmType::OcrDetect, { "algorithm", "baseTask", "text", "action", "sub", "subErrorIgnored", "next", "maxTimes", "exceededNext", "onErrorNext", "preDelay", "rearDelay", "roi", "cache", "rectMove", "reduceOtherTimes", "fullMatch", "ocrReplace", } }, { AlgorithmType::JustReturn, { "algorithm", "baseTask", "action", "sub", "subErrorIgnored", "next", "maxTimes", "exceededNext", "onErrorNext", "preDelay", "rearDelay", "reduceOtherTimes", } }, { AlgorithmType::Hash, { "algorithm", "baseTask", "action", "sub", "subErrorIgnored", "next", "maxTimes", "exceededNext", "onErrorNext", "preDelay", "rearDelay", "roi", "cache", "rectMove", "reduceOtherTimes", "hash", "maskRange", "specialThreshold", "threshold", } }, }; static const std::unordered_map> allowed_key_under_action = { { ProcessTaskAction::ClickRect, { "specificRect", } }, }; auto is_doc = [&](std::string_view key) { return key.find("Doc") != std::string_view::npos || key.find("doc") != std::string_view::npos; }; // 兜底策略,如果某个 key ("xxx") 不符合规范(可能是代码中使用到的参数,而不是任务流程) // 需要加一个注释 ("xxx_Doc") 就能过 syntax_check. auto has_doc = [&](const std::string& key) -> bool { return task_json.find(key + "_Doc") || task_json.find(key + "_doc"); }; bool validity = true; if (!m_all_tasks_info.contains(task_name)) { Log.error("TaskData::syntax_check | Task", task_name, "has not been generated."); return false; } // 获取 algorithm auto algorithm = m_all_tasks_info[task_name]->algorithm; if (algorithm == AlgorithmType::Invalid) [[unlikely]] { Log.error(task_name, "has unknown algorithm."); validity = false; } // 获取 action auto action = m_all_tasks_info[task_name]->action; if (action == ProcessTaskAction::Invalid) [[unlikely]] { Log.error(task_name, "has unknown action."); validity = false; } std::unordered_set allowed_key {}; if (allowed_key_under_algorithm.contains(algorithm)) { decltype(allowed_key) tmp = allowed_key_under_algorithm.at(algorithm); allowed_key.merge(tmp); } if (allowed_key_under_action.contains(action)) { decltype(allowed_key) tmp = allowed_key_under_action.at(action); allowed_key.merge(tmp); } for (const auto& name : task_json.as_object() | views::keys) { if (!allowed_key.contains(name) && !is_doc(name) && !has_doc(name)) { Log.error(task_name, "has unknown key:", name); validity = false; } } return validity; } #endif