diff --git a/resource/global/YoStarEN/resource/tasks.json b/resource/global/YoStarEN/resource/tasks.json index d768ea164e..9b8892a94f 100644 --- a/resource/global/YoStarEN/resource/tasks.json +++ b/resource/global/YoStarEN/resource/tasks.json @@ -2204,7 +2204,7 @@ 208 ] }, - "ExpiringMedicineCheck": { + "MedicineExpiringTime": { "text": [ "min", "hr", diff --git a/resource/global/YoStarJP/resource/tasks.json b/resource/global/YoStarJP/resource/tasks.json index 0f12bfbdc5..ef7b8cd5d4 100644 --- a/resource/global/YoStarJP/resource/tasks.json +++ b/resource/global/YoStarJP/resource/tasks.json @@ -2332,7 +2332,7 @@ "残り配置可能数" ] }, - "ExpiringMedicineCheck": { + "MedicineExpiringTime": { "text": [ "分", "時間", @@ -5777,4 +5777,4 @@ "Stop" ] } -} \ No newline at end of file +} diff --git a/resource/global/YoStarKR/resource/tasks.json b/resource/global/YoStarKR/resource/tasks.json index 35a87ae6d3..d233527261 100644 --- a/resource/global/YoStarKR/resource/tasks.json +++ b/resource/global/YoStarKR/resource/tasks.json @@ -2297,7 +2297,7 @@ "인원" ] }, - "ExpiringMedicineCheck": { + "MedicineExpiringTime": { "text": [ "1일", "시간", @@ -5892,4 +5892,4 @@ ] ] } -} \ No newline at end of file +} diff --git a/resource/global/txwy/resource/tasks.json b/resource/global/txwy/resource/tasks.json index d638d80671..1bee21c3e5 100644 --- a/resource/global/txwy/resource/tasks.json +++ b/resource/global/txwy/resource/tasks.json @@ -1441,7 +1441,7 @@ "行動結束" ] }, - "ExpiringMedicineCheck": { + "MedicineExpiringTime": { "text": [ "分鐘", "小時", @@ -4615,4 +4615,4 @@ ] ] } -} \ No newline at end of file +} diff --git a/resource/tasks.json b/resource/tasks.json index 8b7b096bba..149616d1f2 100644 --- a/resource/tasks.json +++ b/resource/tasks.json @@ -4109,51 +4109,205 @@ 400, 100 ], + "reduceOtherTimes": [ + "StartButton1" + ], "next": [ - "MedicineConfirm" + "StartButton1", + "OfflineConfirm", + "CloseStonePage" ] }, "MedicineConfirm": { "action": "ClickSelf", - "maxTimes": 0, "roi": [ 1000, 500, 200, 150 ], - "reduceOtherTimes": [ - "StartButton1" - ], - "exceededNext": [ - "ExpiringMedicineCheck", - "CloseStonePage" - ], "next": [ - "StartButton1", - "OfflineConfirm", - "MedicineConfirmAgain@LoadingText" + "#self", + "Stop" ] }, - "MedicineConfirmAgain": { - "Doc": "Refer to Task `StoneConfirmAgain`", - "baseTask": "MedicineConfirm", - "template": "MedicineConfirm.png", - "maxTimes": 2147483647, - "reduceOtherTimes": [] - }, - "UseStone": { - "action": "DoNothing", - "roi": [ - 600, - 130, - 420, - 420 - ], - "next": [ - "StoneConfirm" + "MedicineReduceIcon":{ + "preDelay": 500, + "roi": [ + 750, + 200, + 500, + 50 + ], + "maskRange": [ + 1, + 255 + ] + }, + "MedicineUsingCount":{ + "algorithm": "OcrDetect", + "text": [], + "isAscii": true, + "rectMove": [ + -90, + 28, + 100, + 60 + ], + "ocrReplace": [ + [ + "\\|", + "/" + ], + [ + "o", + "0" + ], + [ + "O", + "0" + ], + [ + "I", + "1" + ], + [ + "l", + "1" + ] ] - }, + }, + "MedicineInventory":{ + "algorithm": "OcrDetect", + "text": [], + "isAscii": true, + "rectMove": [ + -29, + 87, + 40, + 33 + ], + "ocrReplace": [ + [ + "\\|", + "/" + ], + [ + "o", + "0" + ], + [ + "O", + "0" + ], + [ + "I", + "1" + ], + [ + "l", + "1" + ] + ] + }, + "MedicineExpiringTime":{ + "algorithm": "OcrDetect", + "text": [ + "分钟", + "小时", + "1天" + ], + "rectMove": [ + -60, + 128, + 75, + 30 + ], + "ocrReplace": [ + [ + ".+小时", + "小时" + ], + [ + ".+分钟", + "分钟" + ], + [ + "^\\d+小$", + "小时" + ], + [ + "^\\d+分$", + "分钟" + ] + ] + }, + "UsingMedicine-Target":{ + "algorithm": "OcrDetect", + "roi": [ + 255, + 390, + 315, + 140 + ], + "text": [], + "isAscii": true, + "ocrReplace": [ + [ + "\\|", + "/" + ], + [ + "o", + "0" + ], + [ + "O", + "0" + ], + [ + "I", + "1" + ], + [ + "l", + "1" + ] + ] + }, + "UsingMedicine-SanityMax":{ + "algorithm": "OcrDetect", + "roi": [ + 440, + 330, + 150, + 55 + ], + "text": [], + "isAscii": true, + "ocrReplace": [ + [ + "\\|", + "/" + ], + [ + "o", + "0" + ], + [ + "O", + "0" + ], + [ + "I", + "1" + ], + [ + "l", + "1" + ] + ] + }, "DrGrandetUseOriginiums": { "algorithm": "OcrDetect", "text": [], @@ -4227,6 +4381,18 @@ "ExpiringMedicineConfirm@LoadingText" ] }, + "UseStone": { + "action": "DoNothing", + "roi": [ + 600, + 130, + 420, + 420 + ], + "next": [ + "StoneConfirm" + ] + }, "StoneConfirm": { "template": "MedicineConfirm.png", "action": "ClickSelf", diff --git a/resource/template/MedicineReduceIcon.png b/resource/template/MedicineReduceIcon.png new file mode 100644 index 0000000000..66844cae89 Binary files /dev/null and b/resource/template/MedicineReduceIcon.png differ diff --git a/src/MaaCore/MaaCore.vcxproj b/src/MaaCore/MaaCore.vcxproj index 6650b2f15f..9ab909a0d6 100644 --- a/src/MaaCore/MaaCore.vcxproj +++ b/src/MaaCore/MaaCore.vcxproj @@ -67,6 +67,7 @@ + @@ -236,6 +237,7 @@ + diff --git a/src/MaaCore/MaaCore.vcxproj.filters b/src/MaaCore/MaaCore.vcxproj.filters index 109baf578b..d1c257a0bb 100644 --- a/src/MaaCore/MaaCore.vcxproj.filters +++ b/src/MaaCore/MaaCore.vcxproj.filters @@ -657,6 +657,9 @@ Source\Task\Roguelike + + Source\Task\Fight + Source\Task\Miscellaneous @@ -1094,5 +1097,8 @@ Source\Task\Miscellaneous + + Source\Task\Fight + \ No newline at end of file diff --git a/src/MaaCore/Task/Fight/DrGrandetTaskPlugin.cpp b/src/MaaCore/Task/Fight/DrGrandetTaskPlugin.cpp index 8f08bdfc61..9d14c6b47b 100644 --- a/src/MaaCore/Task/Fight/DrGrandetTaskPlugin.cpp +++ b/src/MaaCore/Task/Fight/DrGrandetTaskPlugin.cpp @@ -14,7 +14,7 @@ bool asst::DrGrandetTaskPlugin::verify(AsstMsg msg, const json::value& details) } const std::string task = details.at("details").at("task").as_string(); - if (task == "Fight@StoneConfirm" || task == "Fight@MedicineConfirm") { + if (task == "Fight@StoneConfirm") { return true; } else { @@ -26,7 +26,7 @@ bool asst::DrGrandetTaskPlugin::_run() { LogTraceFunction; - int cur_ms = analyze_time_left(); + int cur_ms = analyze_time_left(ctrler()->get_image()); if (cur_ms < 0) { return false; } @@ -45,11 +45,11 @@ bool asst::DrGrandetTaskPlugin::_run() return false; } -int asst::DrGrandetTaskPlugin::analyze_time_left() +int asst::DrGrandetTaskPlugin::analyze_time_left(const cv::Mat& image) { LogTraceFunction; - OCRer analyzer(ctrler()->get_image()); + OCRer analyzer(image); analyzer.set_task_info("DrGrandetUseOriginiums"); analyzer.set_replace(Task.get("NumberOcrReplace")->replace_map); // 这里是汉字和数字混合的,用不了单独的en模型 diff --git a/src/MaaCore/Task/Fight/DrGrandetTaskPlugin.h b/src/MaaCore/Task/Fight/DrGrandetTaskPlugin.h index 3ae661bed7..12ed948020 100644 --- a/src/MaaCore/Task/Fight/DrGrandetTaskPlugin.h +++ b/src/MaaCore/Task/Fight/DrGrandetTaskPlugin.h @@ -14,11 +14,11 @@ namespace asst virtual bool verify(AsstMsg msg, const json::value& details) const override; - private: - virtual bool _run() override; - // 识别“理智将在x:xx后恢复”,返回 x:xx 对应的 毫秒 数 // 若识别失败返回 < 0 - int analyze_time_left(); + static int analyze_time_left(const cv::Mat& img); + + private: + virtual bool _run() override; }; } diff --git a/src/MaaCore/Task/Fight/MedicineCounterPlugin.cpp b/src/MaaCore/Task/Fight/MedicineCounterPlugin.cpp new file mode 100644 index 0000000000..39725d4920 --- /dev/null +++ b/src/MaaCore/Task/Fight/MedicineCounterPlugin.cpp @@ -0,0 +1,208 @@ +#include "MedicineCounterPlugin.h" + +#include + +#include "Config/TaskData.h" +#include "Controller/Controller.h" +#include "InstHelper.h" +#include "Task/Fight/DrGrandetTaskPlugin.h" +#include "Task/ProcessTask.h" +#include "Utils/Logger.hpp" +#include "Utils/NoWarningCV.h" +#include "Vision/MultiMatcher.h" +#include "Vision/RegionOCRer.h" + +bool asst::MedicineCounterPlugin::verify(AsstMsg msg, const json::value& details) const +{ + if (msg != AsstMsg::SubTaskStart || details.get("subtask", std::string()) != "ProcessTask") { + return false; + } + + return details.at("details").at("task").as_string().ends_with("UseMedicine"); +} + +bool asst::MedicineCounterPlugin::_run() +{ + LogTraceFunction; + + if (m_using_count >= m_max_count && !m_use_expiring) { + return true; + } + + // 现在葛朗台插件在前,暂时不需要延迟 + // sleep(Task.get("MedicineReduceIcon")->pre_delay); + auto image = ctrler()->get_image(); + + // 预计最大使用药品情况不会超过2种,不考虑滑动 + // 10+10+N + auto initialCount = initial_count(image); + if (!initialCount) [[unlikely]] { + return false; + } + + if (m_using_count < m_max_count) { + if (initialCount->using_count + m_using_count > m_max_count) { + reduce_excess(*initialCount); + } + + image = ctrler()->get_image(); + auto count = initial_count(image); + if (!count || count->using_count + m_using_count > m_max_count) [[unlikely]] { + Log.error(__FUNCTION__, "use medicine over limit:", count ? count->using_count : -1); + return false; + } + initialCount = count; + } + else if (m_use_expiring) { + for (auto& [use, inventory, rect, is_expiring] : initialCount->medicines | std::ranges::views::reverse) { + while (use > 0 && !is_expiring) { + ctrler()->click(rect); + sleep(Config.get_options().task_delay); + use--; + } + } + + image = ctrler()->get_image(); + auto count = initial_count(image); + if (!count || count->using_count + m_using_count > m_max_count) [[unlikely]] { + Log.error(__FUNCTION__, "use medicine over limit:", count ? count->using_count : -1); + return false; + } + initialCount = count; + } + m_using_count += initialCount->using_count; + + // 博朗台:如果溢出则等待 + if (m_dr_grandet) { + auto sanity_target = get_target_of_sanity(image); + auto sanity_max = get_maximun_of_sanity(image); + if (!sanity_target || !sanity_max) [[unlikely]] { + return false; + } + + if (*sanity_target >= *sanity_max) [[unlikely]] { + auto waitTime = DrGrandetTaskPlugin::analyze_time_left(image); + sleep(waitTime); + } + } + + ProcessTask(*this, { "MedicineConfirm" }).run(); + + auto info = basic_info_with_what("UseMedicine"); + info["detail"]["is_expiring"] = m_using_count > m_max_count; + info["detail"]["count"] = initialCount->using_count; + callback(AsstMsg::SubTaskExtraInfo, info); + return true; +} + +std::optional asst::MedicineCounterPlugin::initial_count( + cv::Mat image) +{ + int use = 0; + MultiMatcher multiMatcher(image); + multiMatcher.set_task_info("MedicineReduceIcon"); + if (!multiMatcher.analyze()) { + Log.error(__FUNCTION__, "medicine reduce icon analyze failed"); + return std::nullopt; + } + + std::vector medicines; + for (const auto& result : multiMatcher.get_result()) { + auto using_rect = result.rect.move(Task.get("MedicineUsingCount")->rect_move); + auto inventory_rect = result.rect.move(Task.get("MedicineInventory")->rect_move); + auto expiring_rect = result.rect.move(Task.get("MedicineExpiringTime")->rect_move); + cv::rectangle(image, make_rect(using_rect), cv::Scalar(0, 0, 255), 1); + cv::rectangle(image, make_rect(inventory_rect), cv::Scalar(0, 0, 255), 1); + cv::rectangle(image, make_rect(expiring_rect), cv::Scalar(0, 0, 255), 1); + + RegionOCRer using_ocr(image); + using_ocr.set_task_info("MedicineUsingCount"); + using_ocr.set_bin_threshold(100, 255); + using_ocr.set_roi(using_rect); + if (!using_ocr.analyze()) { + Log.error(__FUNCTION__, "medicine using count analyze failed"); + return std::nullopt; + } + + RegionOCRer inventry_ocr(image); + inventry_ocr.set_task_info("MedicineUsingCount"); + inventry_ocr.set_bin_threshold(100, 255); + inventry_ocr.set_roi(inventory_rect); + if (!inventry_ocr.analyze()) { + Log.error(__FUNCTION__, "medicine using count analyze failed"); + return std::nullopt; + } + + RegionOCRer expiring_ocr(image); + expiring_ocr.set_task_info("MedicineExpiringTime"); + expiring_ocr.set_bin_threshold(100, 255); + expiring_ocr.set_roi(expiring_rect); + if (!expiring_ocr.analyze()) { + Log.error(__FUNCTION__, "medicine expiring time analyze failed"); + return std::nullopt; + } + bool is_expiring = + ranges::any_of(Task.get("MedicineExpiringTime")->text, + [&](const std::string_view& sv) { return sv == expiring_ocr.get_result().text; }); + + int using_count = 0, inventory_count = 0; + if (!utils::chars_to_number(using_ocr.get_result().text, using_count) || + !utils::chars_to_number(inventry_ocr.get_result().text, inventory_count)) { + return std::nullopt; + } + use += using_count; + medicines.emplace_back(Medicine { .use = using_count, + .inventry = inventory_count, + .reduce_button_position = result.rect, + .is_expiring = is_expiring }); + } + return InitialMedicineResult { .using_count = use, .medicines = medicines }; +} + +void asst::MedicineCounterPlugin::reduce_excess(InitialMedicineResult& using_medicine) +{ + auto reduce = m_using_count + using_medicine.using_count - m_max_count; + for (auto& [use, inventory, rect, is_expiring] : using_medicine.medicines | std::ranges::views::reverse) { + while (use > 0 && reduce > 0) { + ctrler()->click(rect); + sleep(Config.get_options().task_delay); + use--; + reduce--; + using_medicine.using_count--; + } + } +} + +std::optional asst::MedicineCounterPlugin::get_target_of_sanity(const cv::Mat& image) +{ + RegionOCRer ocr(image); + ocr.set_bin_threshold(100, 255); + ocr.set_task_info("UsingMedicine-Target"); + if (!ocr.analyze()) [[unlikely]] { + Log.error(__FUNCTION__, "unable to ocr"); + return std::nullopt; + } + int num = 0; + if (!utils::chars_to_number(ocr.get_result().text, num)) { + Log.error(__FUNCTION__, "unable to change [", ocr.get_result().text, "] into int"); + return false; + } + return num; +} + +std::optional asst::MedicineCounterPlugin::get_maximun_of_sanity(const cv::Mat& image) +{ + RegionOCRer ocr(image); + ocr.set_bin_threshold(100, 255); + ocr.set_task_info("UsingMedicine-SanityMax"); + if (!ocr.analyze()) [[unlikely]] { + Log.error(__FUNCTION__, "unable to ocr"); + return std::nullopt; + } + int num = 0; + if (!utils::chars_to_number(ocr.get_result().text, num)) { + Log.error(__FUNCTION__, "unable to change [", ocr.get_result().text, "] into int"); + return false; + } + return num; +} diff --git a/src/MaaCore/Task/Fight/MedicineCounterPlugin.h b/src/MaaCore/Task/Fight/MedicineCounterPlugin.h new file mode 100644 index 0000000000..a0741a6d7a --- /dev/null +++ b/src/MaaCore/Task/Fight/MedicineCounterPlugin.h @@ -0,0 +1,47 @@ +#pragma once +#include "Task/AbstractTaskPlugin.h" + +#include "Common/AsstTypes.h" + +namespace asst +{ + class MedicineCounterPlugin final : public AbstractTaskPlugin + { + public: + + using AbstractTaskPlugin::AbstractTaskPlugin; + virtual ~MedicineCounterPlugin() override = default; + virtual bool verify(AsstMsg msg, const json::value& details) const override; + void set_dr_grandet(bool dr_grandet) { m_dr_grandet = dr_grandet; }; + void set_count(int count) { m_max_count = count; } + void set_use_expiring(bool use_expiring) { m_use_expiring = use_expiring; } + + private: + virtual bool _run() override; + + struct Medicine + { + int use; + int inventry; + asst::Rect reduce_button_position; + bool is_expiring; + }; + + // 库存量, 移除按钮的位置 + struct InitialMedicineResult + { + int using_count; + std::vector medicines; + }; + + // 识别初始使用的药量 + std::optional initial_count(cv::Mat image); + // 减少药品使用 + void reduce_excess(InitialMedicineResult& using_medicine); + std::optional get_target_of_sanity(const cv::Mat& image); + std::optional get_maximun_of_sanity(const cv::Mat& image); + + bool m_use_expiring,m_dr_grandet = false; + int m_using_count = 0, m_max_count = 0; + }; +} diff --git a/src/MaaCore/Task/Fight/SanityBeforeStagePlugin.h b/src/MaaCore/Task/Fight/SanityBeforeStagePlugin.h index 03e75d9953..a6c8a24260 100644 --- a/src/MaaCore/Task/Fight/SanityBeforeStagePlugin.h +++ b/src/MaaCore/Task/Fight/SanityBeforeStagePlugin.h @@ -4,7 +4,6 @@ #include namespace asst { - class SanityBeforeStagePlugin final : public AbstractTaskPlugin { public: diff --git a/src/MaaCore/Task/Interface/FightTask.cpp b/src/MaaCore/Task/Interface/FightTask.cpp index 01537e6754..fd0c50e229 100644 --- a/src/MaaCore/Task/Interface/FightTask.cpp +++ b/src/MaaCore/Task/Interface/FightTask.cpp @@ -8,6 +8,7 @@ #include "Task/Fight/SideStoryReopenTask.h" #include "Task/Fight/StageDropsTaskPlugin.h" #include "Task/Fight/StageNavigationTask.h" +#include "Task/Fight/MedicineCounterPlugin.h" #include "Task/ProcessTask.h" #include "Utils/Logger.hpp" #include "Utils/Ranges.hpp" @@ -26,7 +27,6 @@ asst::FightTask::FightTask(const AsstCallback& callback, Assistant* inst) // 对于当前/上次,就是点到 蓝色开始行动 为止。 m_start_up_task_ptr->set_times_limit("StartButton1", 0) .set_times_limit("StartButton2", 0) - .set_times_limit("MedicineConfirm", 0) .set_times_limit("StoneConfirm", 0) .set_times_limit("StageSNReturnFlag", 0) .set_times_limit("PRTS1", 0) @@ -50,6 +50,7 @@ asst::FightTask::FightTask(const AsstCallback& callback, Assistant* inst) m_dr_grandet_task_plugin_ptr = m_fight_task_ptr->register_plugin(); m_dr_grandet_task_plugin_ptr->set_enable(false); m_fight_task_ptr->register_plugin(); + m_medicine_plugin = m_fight_task_ptr->register_plugin(); m_subtasks.emplace_back(m_start_up_task_ptr); m_subtasks.emplace_back(m_stage_navigation_task_ptr); @@ -114,6 +115,9 @@ bool asst::FightTask::set_params(const json::value& params) .set_times_limit("StoneConfirm", stone) .set_times_limit("StartButton1", times) .set_times_limit("StartButton2", times); + m_medicine_plugin->set_count(medicine); + m_medicine_plugin->set_use_expiring(expiring_medicine != 0); + m_medicine_plugin->set_dr_grandet(is_dr_grandet); m_dr_grandet_task_plugin_ptr->set_enable(is_dr_grandet); m_stage_drops_plugin_ptr->set_enable_penguid(enable_penguid); m_stage_drops_plugin_ptr->set_penguin_id(penguin_id); diff --git a/src/MaaCore/Task/Interface/FightTask.h b/src/MaaCore/Task/Interface/FightTask.h index 80f2727bc1..d3f11c1fbc 100644 --- a/src/MaaCore/Task/Interface/FightTask.h +++ b/src/MaaCore/Task/Interface/FightTask.h @@ -10,6 +10,7 @@ namespace asst class StageNavigationTask; class DrGrandetTaskPlugin; class SideStoryReopenTask; + class MedicineCounterPlugin; class FightTask final : public InterfaceTask { @@ -25,6 +26,7 @@ namespace asst std::shared_ptr m_start_up_task_ptr = nullptr; std::shared_ptr m_stage_navigation_task_ptr = nullptr; std::shared_ptr m_fight_task_ptr = nullptr; + std::shared_ptr m_medicine_plugin = nullptr; std::shared_ptr m_stage_drops_plugin_ptr = nullptr; std::shared_ptr m_dr_grandet_task_plugin_ptr = nullptr; std::shared_ptr m_sidestory_reopen_task_ptr = nullptr; diff --git a/src/MaaWpfGui/Main/AsstProxy.cs b/src/MaaWpfGui/Main/AsstProxy.cs index f979086c85..27f95eab1f 100644 --- a/src/MaaWpfGui/Main/AsstProxy.cs +++ b/src/MaaWpfGui/Main/AsstProxy.cs @@ -816,16 +816,6 @@ namespace MaaWpfGui.Main Instances.TaskQueueViewModel.AddLog(log.TrimEnd('\n').TrimEnd(' '), UiLogColor.Info); break; - case "MedicineConfirm": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("MedicineUsed") + $" {execTimes} " + LocalizationHelper.GetString("UnitTime"), UiLogColor.Info); - MedicineUsedTimes++; - break; - - case "ExpiringMedicineConfirm": - Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("ExpiringMedicineUsed") + $" {execTimes} " + LocalizationHelper.GetString("UnitTime"), UiLogColor.Info); - ExpiringMedicineUsedTimes++; - break; - case "StoneConfirm": Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("StoneUsed") + $" {execTimes} " + LocalizationHelper.GetString("UnitTime"), UiLogColor.Info); StoneUsedTimes++; @@ -1265,8 +1255,8 @@ namespace MaaWpfGui.Main break; } - var sanityCur = sanityReport.TryGetValue("current_sanity", out var sanityCurToken) ? (int)sanityCurToken : -1; - var sanityMax = sanityReport.TryGetValue("max_sanity", out var sanityMaxToken) ? (int)sanityMaxToken : -1; + int sanityCur = sanityReport.TryGetValue("current_sanity", out var sanityCurToken) ? (int)sanityCurToken : -1; + int sanityMax = sanityReport.TryGetValue("max_sanity", out var sanityMaxToken) ? (int)sanityMaxToken : -1; var reportTime = sanityReport.TryGetValue("report_time", out var reportTimeToken) ? (string)reportTimeToken : string.Empty; if (sanityCur < 0 || sanityMax < 1 || reportTime?.Length < 12) { @@ -1288,6 +1278,38 @@ namespace MaaWpfGui.Main SanityReport.HasSanityReport = true; break; + case "UseMedicine": + var medicineReport = (JObject)subTaskDetails; + if (medicineReport is null || !medicineReport.ContainsKey("is_expiring") || !medicineReport.ContainsKey("count")) + { + // todo addlog + break; + } + + bool isExpiringMedicine = medicineReport.TryGetValue("is_expiring", out var isExpiringMedicineToken) ? (bool)isExpiringMedicineToken : false; + int medicineCount = medicineReport.TryGetValue("count", out var medicineCountToken) ? (int)medicineCountToken : -1; + + if (medicineCount == -1) + { + Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("MedicineUsed") + $" Unknown times", UiLogColor.Error); + break; + } + + var medicineLog = string.Empty; + if (!isExpiringMedicine) + { + MedicineUsedTimes += medicineCount; + medicineLog = LocalizationHelper.GetString("MedicineUsed") + $" {MedicineUsedTimes}(+{medicineCount})"; + } + else + { + ExpiringMedicineUsedTimes += medicineCount; + medicineLog = LocalizationHelper.GetString("ExpiringMedicineUsed") + $" {ExpiringMedicineUsedTimes}(+{medicineCount})"; + } + + Instances.TaskQueueViewModel.AddLog(medicineLog, UiLogColor.Info); + break; + case "StageQueueUnableToAgent": Instances.TaskQueueViewModel.AddLog(LocalizationHelper.GetString("StageQueue") + $" {subTaskDetails["stage_code"]} " + LocalizationHelper.GetString("UnableToAgent"), UiLogColor.Info); break;