mirror of
https://github.com/MaaAssistantArknights/MaaAssistantArknights.git
synced 2026-07-05 04:10:26 +08:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf570edfc7 | ||
|
|
08176f1e72 | ||
|
|
2f5b6528cc | ||
|
|
a5e468a608 |
2
.github/workflows/release-preparation.yml
vendored
2
.github/workflows/release-preparation.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Checkout dev-v2 with full history
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
ref: dev-v2
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -295,7 +295,7 @@
|
||||
"fullMatch": true,
|
||||
"isAscii": true,
|
||||
"text": [],
|
||||
"ocrReplaceDoc": "和 ClickedCorrectStage 同步",
|
||||
"ocrReplaceDoc": "和 ClickStageName 同步",
|
||||
"ocrReplace": [
|
||||
["OPERATION", ""],
|
||||
["-\\]", "-1"],
|
||||
@@ -313,6 +313,33 @@
|
||||
],
|
||||
"roi": [845, 72, 220, 50]
|
||||
},
|
||||
"StageNavigationByTemplateMatchBegin": {
|
||||
"doc": "模板匹配导航入口(对应 OCR 的 StageNavigationBegin)",
|
||||
"algorithm": "JustReturn",
|
||||
"next": ["ClickStageByTemplate", "FullStageNavigationByTemplate"]
|
||||
},
|
||||
"FullStageNavigationByTemplate": {
|
||||
"doc": "模板匹配导航右滑+左滑(对应 OCR 的 FullStageNavigation)",
|
||||
"baseTask": "FullStageNavigation",
|
||||
"exceededNext": ["ClickStageByTemplate", "StageNavigationSlowlySwipeLeftByTemplate"]
|
||||
},
|
||||
"ClickStageByTemplate": {
|
||||
"doc": "模板匹配关卡截图(对应 OCR 的 ClickStageName),由 C++ 动态替换实际模板",
|
||||
"action": "ClickSelf",
|
||||
"template": ["empty.png"],
|
||||
"roi": [0, 0, 1280, 720],
|
||||
"next": ["ClickedCorrectStageByTemplateOrSwipe", "#self"]
|
||||
},
|
||||
"StageNavigationSlowlySwipeLeftByTemplate": {
|
||||
"doc": "模板匹配导航左滑找关(对应 OCR 的 StageNavigationSlowlySwipeLeft)",
|
||||
"baseTask": "StageNavigationSlowlySwipeLeft",
|
||||
"next": ["FullStageNavigationByTemplate"]
|
||||
},
|
||||
"ClickedCorrectStageByTemplateOrSwipe": {
|
||||
"doc": "模板匹配后验证(对应 OCR 的 ClickedCorrectStageOrSwipe)",
|
||||
"baseTask": "ClickedCorrectStageOrSwipe",
|
||||
"next": ["ClickedCorrectStage", "FullStageNavigationByTemplate"]
|
||||
},
|
||||
"ChangeToRaidDifficulty": {
|
||||
"doc": ["突袭", "15章险地"],
|
||||
"template": ["RaidDifficulty.png", "RaidDifficulty-Chapter15.png"],
|
||||
|
||||
BIN
resource/template/StageNavigation/SideStory/TD/TD-1.png
Normal file
BIN
resource/template/StageNavigation/SideStory/TD/TD-1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
BIN
resource/template/StageNavigation/SideStory/TD/TD-2.png
Normal file
BIN
resource/template/StageNavigation/SideStory/TD/TD-2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
BIN
resource/template/StageNavigation/SideStory/TD/TD-3.png
Normal file
BIN
resource/template/StageNavigation/SideStory/TD/TD-3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
BIN
resource/template/StageNavigation/SideStory/TD/TD-4.png
Normal file
BIN
resource/template/StageNavigation/SideStory/TD/TD-4.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
BIN
resource/template/StageNavigation/SideStory/TD/TD-5.png
Normal file
BIN
resource/template/StageNavigation/SideStory/TD/TD-5.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
BIN
resource/template/StageNavigation/SideStory/TD/TD-P-1.png
Normal file
BIN
resource/template/StageNavigation/SideStory/TD/TD-P-1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
@@ -99,6 +99,18 @@ bool asst::TemplResource::load(const std::filesystem::path& path)
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
// 将所有图片按完整相对路径注册到索引,支持 C++ 代码动态引用(如活动关卡截图模板)
|
||||
// 仅当该路径尚未被注册时才插入,不影响已有的 m_load_required 匹配逻辑
|
||||
for (const auto& [rel_path, full_path] : relative_path_index) {
|
||||
if (!rel_path.ends_with(".png")) {
|
||||
continue;
|
||||
}
|
||||
if (!m_templ_paths.contains(rel_path)) {
|
||||
m_templ_paths.emplace(rel_path, full_path);
|
||||
}
|
||||
}
|
||||
|
||||
m_templs.clear();
|
||||
++m_revision;
|
||||
return true;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "Task/Fight/MedicineCounterTaskPlugin.h"
|
||||
#include "Task/Fight/StageQueueMissionCompletedTaskPlugin.h"
|
||||
#include "Task/ProcessTask.h"
|
||||
#include "Task/StageNavigationHelper.h"
|
||||
#include "Utils/Logger.hpp"
|
||||
|
||||
void asst::SideStoryReopenTask::set_sidestory_name(std::string sidestory_name)
|
||||
@@ -179,6 +180,15 @@ bool asst::SideStoryReopenTask::select_stage(int stage_index)
|
||||
|
||||
const auto& m_stage_code = m_sidestory_name + "-" + std::to_string(stage_index);
|
||||
|
||||
// 优先检查是否存在对应活动关卡名的模板资源,如果存在则走模板匹配
|
||||
std::string templ_path = StageNavigationHelper::get_stage_template_path(m_stage_code);
|
||||
if (!templ_path.empty()) {
|
||||
Log.info("Stage template found, using template matching for", m_stage_code, ", templ:", templ_path);
|
||||
Task.get<MatchTaskInfo>(m_stage_code + "@ClickStageByTemplate")->templ_names = { templ_path + ".png" };
|
||||
Task.get<OcrTaskInfo>(m_stage_code + "@ClickedCorrectStageByTemplateOrSwipe")->text = { m_stage_code };
|
||||
return ProcessTask(*this, { m_stage_code + "@StageNavigationByTemplateMatchBegin" }).run();
|
||||
}
|
||||
|
||||
Task.get<OcrTaskInfo>(m_stage_code + "@ClickStageName")->text = { m_stage_code };
|
||||
Task.get<OcrTaskInfo>(m_stage_code + "@ClickedCorrectStage")->text = { m_stage_code };
|
||||
return ProcessTask(*this, { m_stage_code + "@StageNavigationBegin" }).run();
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "Config/TaskData.h"
|
||||
#include "Controller/Controller.h"
|
||||
#include "Task/ProcessTask.h"
|
||||
#include "Task/StageNavigationHelper.h"
|
||||
#include "Utils/Logger.hpp"
|
||||
#include "Vision/OCRer.h"
|
||||
|
||||
@@ -190,6 +191,20 @@ bool asst::StageNavigationTask::swipe_and_find_stage()
|
||||
{
|
||||
LogTraceFunction;
|
||||
|
||||
// 优先检查是否存在对应活动关卡名的模板资源,如果存在则走模板匹配
|
||||
std::string templ_path = StageNavigationHelper::get_stage_template_path(m_stage_code);
|
||||
if (!templ_path.empty()) {
|
||||
Log.info("Stage template found, using template matching for", m_stage_code, ", templ:", templ_path);
|
||||
Task.get<MatchTaskInfo>(m_stage_code + "@ClickStageByTemplate")->templ_names = { templ_path + ".png" };
|
||||
Task.get<OcrTaskInfo>(m_stage_code + "@ClickedCorrectStageByTemplateOrSwipe")->text = { m_stage_code };
|
||||
return ProcessTask(
|
||||
*this,
|
||||
{ m_stage_code + "@StageNavigationByTemplateMatchBegin" })
|
||||
.set_retry_times(RetryTimesDefault)
|
||||
.run();
|
||||
}
|
||||
|
||||
// 无模板,使用 OCR 匹配
|
||||
Task.get<OcrTaskInfo>(m_stage_code + "@ClickStageName")->text = { m_stage_code };
|
||||
std::string replace_m_stage_code = m_stage_code;
|
||||
utils::string_replace_all_in_place(replace_m_stage_code, { { "-", "" } });
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "Controller/Controller.h"
|
||||
#include "Task/Miscellaneous/BattleProcessTask.h"
|
||||
#include "Task/ProcessTask.h"
|
||||
#include "Task/StageNavigationHelper.h"
|
||||
#include "Utils/Logger.hpp"
|
||||
#include "Utils/Platform.hpp"
|
||||
#include "Vision/Matcher.h"
|
||||
@@ -66,6 +67,21 @@ bool asst::MultiCopilotTaskPlugin::_run()
|
||||
|
||||
bool asst::MultiCopilotTaskPlugin::navigate_to_stage(const std::string& stage_name)
|
||||
{
|
||||
// 优先检查是否存在对应活动关卡名的模板资源,如果存在则走模板匹配
|
||||
std::string templ_path = StageNavigationHelper::get_stage_template_path(stage_name);
|
||||
if (!templ_path.empty()) {
|
||||
Log.info("Stage template found, using template matching for", stage_name, ", templ:", templ_path);
|
||||
// 动态注入模板路径到 MatchTaskInfo(需带 .png 后缀)
|
||||
Task.get<MatchTaskInfo>(stage_name + "@Copilot@ClickStageByTemplate")->templ_names = { templ_path + ".png" };
|
||||
Task.get<OcrTaskInfo>(stage_name + "@Copilot@ClickedCorrectStage")->text = { stage_name };
|
||||
return ProcessTask(*this, { stage_name + "@Copilot@StageNavigationByTemplateMatchBegin" })
|
||||
.set_retry_times(20)
|
||||
.run();
|
||||
}
|
||||
|
||||
// 模板不存在,使用基于图像分析的 OCR 方案
|
||||
Log.info("No stage template available, using image-based OCR for", stage_name);
|
||||
|
||||
auto image = ctrler()->get_image();
|
||||
|
||||
if (is_stage_detail_opened(image)) { // 关卡介绍已展开
|
||||
|
||||
46
src/MaaCore/Task/StageNavigationHelper.h
Normal file
46
src/MaaCore/Task/StageNavigationHelper.h
Normal file
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include "Utils/Logger.hpp"
|
||||
#include "Utils/WorkingDir.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
namespace asst::StageNavigationHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 检查活动关卡是否存在对应的模板截图。
|
||||
/// 从关卡名中提取活动前缀(如 "TD-6" -> "TD"),再拼接路径
|
||||
/// resource/template/StageNavigation/SideStory/{PREFIX}/{stage_name}.png
|
||||
/// </summary>
|
||||
/// <param name="stage_name">关卡名,如 "TD-6"、"SN-10"</param>
|
||||
/// <returns>模板相对路径(不含扩展名),如 "StageNavigation/SideStory/TD/TD-6";不存在则返回空字符串</returns>
|
||||
inline std::string get_stage_template_path(const std::string& stage_name)
|
||||
{
|
||||
// 从关卡名中提取活动前缀,例如 "TD-6" -> "TD","SN-10" -> "SN"
|
||||
// 匹配格式:2~3个字母前缀 + "-" + 数字
|
||||
auto dash_pos = stage_name.find('-');
|
||||
if (dash_pos == std::string::npos || dash_pos < 2 || dash_pos > 3) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string prefix = stage_name.substr(0, dash_pos);
|
||||
// 验证前缀全是字母
|
||||
if (!std::all_of(prefix.begin(), prefix.end(), [](char c) { return std::isalpha(static_cast<unsigned char>(c)); })) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// 构造模板相对路径:StageNavigation/SideStory/{PREFIX}/{STAGE_NAME}
|
||||
std::string templ_rel_path = "StageNavigation/SideStory/" + prefix + "/" + stage_name;
|
||||
std::filesystem::path templ_file = ResDir.get() / "template" / (templ_rel_path + ".png");
|
||||
|
||||
if (std::filesystem::exists(templ_file)) {
|
||||
Log.info("Stage template found:", templ_rel_path);
|
||||
return templ_rel_path;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
} // namespace asst::StageNavigationHelper
|
||||
@@ -86,6 +86,12 @@ public class GUI : INotifyPropertyChanged
|
||||
|
||||
public string BackgroundMonetCustomColor { get; set; } = "#326CF3";
|
||||
|
||||
/// <summary>
|
||||
/// 自动取色模式上次提取到的主色(HEX),用于下次启动时同步恢复调色板,避免闪烁。
|
||||
/// 自定义模式的颜色也写入此缓存,使启动时不论何种模式都能即时恢复。
|
||||
/// </summary>
|
||||
public string BackgroundMonetCachedColor { get; set; } = string.Empty;
|
||||
|
||||
[UsedImplicitly]
|
||||
public void OnPropertyChanged(string propertyName, object before, object after)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<Style x:Key="ErrorHighlightCheckBoxStyle" BasedOn="{StaticResource {x:Type CheckBox}}" TargetType="CheckBox">
|
||||
<Style
|
||||
x:Key="MaaCheckBoxDefaultStyle"
|
||||
BasedOn="{StaticResource CheckBoxBaseStyle}"
|
||||
TargetType="{x:Type CheckBox}">
|
||||
<Setter Property="Background" Value="{DynamicResource RegionBrushOpacity50}" />
|
||||
</Style>
|
||||
|
||||
<Style BasedOn="{StaticResource MaaCheckBoxDefaultStyle}" TargetType="{x:Type CheckBox}" />
|
||||
|
||||
<Style
|
||||
x:Key="ErrorHighlightCheckBoxStyle"
|
||||
BasedOn="{StaticResource MaaCheckBoxDefaultStyle}"
|
||||
TargetType="CheckBox">
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter Property="Foreground" Value="{DynamicResource ErrorLogBrush}" />
|
||||
|
||||
@@ -361,7 +361,8 @@ public class SettingsViewModel : Screen
|
||||
GuiSettings.SwitchDarkMode();
|
||||
|
||||
// 主题初始化完成后,若莫奈取色已开启,恢复调色板(必须在主题切换之后执行)
|
||||
BackgroundSettings.UpdateMonet();
|
||||
// 跳过防抖延迟,避免界面先闪烁原版颜色再显示莫奈主题
|
||||
BackgroundSettings.UpdateMonet(skipDebounce: true);
|
||||
}
|
||||
|
||||
private void InitConnectConfig()
|
||||
|
||||
@@ -259,6 +259,37 @@ public class BackgroundSettingsUserControlModel : PropertyChangedBase
|
||||
/// </summary>
|
||||
private Color? _lastExtractedColor;
|
||||
|
||||
/// <summary>
|
||||
/// 从配置中读取上次持久化的自动取色主色。
|
||||
/// 启动时先用它同步应用调色板,避免界面先闪烁原版颜色。
|
||||
/// </summary>
|
||||
/// <returns>持久化缓存的颜色;无缓存或格式无效时返回 null。</returns>
|
||||
private static Color? LoadCachedMonetColor()
|
||||
{
|
||||
var hex = ConfigFactory.Root.GUI.BackgroundMonetCachedColor;
|
||||
if (string.IsNullOrEmpty(hex))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return (Color)ColorConverter.ConvertFromString(hex);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将自动提取的主色持久化到配置,供下次启动时同步恢复。
|
||||
/// </summary>
|
||||
private static void SaveCachedMonetColor(Color color)
|
||||
{
|
||||
ConfigFactory.Root.GUI.BackgroundMonetCachedColor = ThemeHelper.Color2HexString(color);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 打开 HandyControl ColorPicker 弹窗让用户选择自定义颜色。
|
||||
/// </summary>
|
||||
@@ -275,6 +306,7 @@ public class BackgroundSettingsUserControlModel : PropertyChangedBase
|
||||
picker.Confirmed += (_, e) => {
|
||||
var color = e.Info;
|
||||
BackgroundMonetCustomColor = ThemeHelper.Color2HexString(color);
|
||||
SaveCachedMonetColor(color);
|
||||
UpdateMonet();
|
||||
dialog.Close();
|
||||
};
|
||||
@@ -302,10 +334,11 @@ public class BackgroundSettingsUserControlModel : PropertyChangedBase
|
||||
/// 根据当前配置执行莫奈取色逻辑。
|
||||
/// 自动 / 自定义模式都会将计算放到后台线程,仅资源写入在 UI 线程。
|
||||
/// </summary>
|
||||
public void UpdateMonet()
|
||||
/// <param name="skipDebounce">是否跳过防抖延迟。初始化时应传 true 以避免界面先闪烁原版颜色。</param>
|
||||
public void UpdateMonet(bool skipDebounce = false)
|
||||
{
|
||||
_monetUpdateCts?.Cancel();
|
||||
_ = UpdateMonetAsync(CancellationToken.None);
|
||||
_ = UpdateMonetAsync(CancellationToken.None, skipDebounce);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -313,16 +346,21 @@ public class BackgroundSettingsUserControlModel : PropertyChangedBase
|
||||
/// 所有异常均在方法内部捕获并记录,避免 fire-and-forget 调用产生未观察异常。
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
private async Task UpdateMonetAsync(CancellationToken cancellationToken)
|
||||
/// <param name="skipDebounce">是否跳过防抖延迟。</param>
|
||||
private async Task UpdateMonetAsync(CancellationToken cancellationToken, bool skipDebounce = false)
|
||||
{
|
||||
// 防抖延迟:等待 150ms,期间若被取消则直接返回
|
||||
try
|
||||
// 初始化时跳过延迟,避免界面先显示原版颜色再切换为莫奈主题(闪烁)
|
||||
if (!skipDebounce)
|
||||
{
|
||||
await Task.Delay(MonetDebounceMs, cancellationToken).ConfigureAwait(true);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
try
|
||||
{
|
||||
await Task.Delay(MonetDebounceMs, cancellationToken).ConfigureAwait(true);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
@@ -334,6 +372,14 @@ public class BackgroundSettingsUserControlModel : PropertyChangedBase
|
||||
return;
|
||||
}
|
||||
|
||||
// 启动时先用持久化缓存的主色同步应用调色板,避免界面先闪烁原版颜色
|
||||
if (skipDebounce && LoadCachedMonetColor() is { } cached)
|
||||
{
|
||||
_lastExtractedColor = cached;
|
||||
ThemeHelper.ApplyMonetPalette(cached, BackgroundOpacity);
|
||||
NotifyOfPropertyChange(nameof(CurrentMonetColor));
|
||||
}
|
||||
|
||||
if (BackgroundMonetMode == MonetModeType.Custom)
|
||||
{
|
||||
// 自定义模式:直接用用户选定的颜色,计算放后台线程
|
||||
@@ -380,6 +426,9 @@ public class BackgroundSettingsUserControlModel : PropertyChangedBase
|
||||
// 直接使用提取到的原始主色生成调色板
|
||||
_lastExtractedColor = rawColor;
|
||||
|
||||
// 持久化提取结果,下次启动时可同步恢复,避免闪烁
|
||||
SaveCachedMonetColor(rawColor);
|
||||
|
||||
await ThemeHelper.ApplyMonetPaletteAsync(rawColor, BackgroundOpacity, cancellationToken);
|
||||
|
||||
NotifyOfPropertyChange(nameof(CurrentMonetColor));
|
||||
|
||||
@@ -4166,5 +4166,11 @@
|
||||
"resource/global/txwy/resource/template/MiniGame/SPA/MiniGame@SPA@ReturnHome": "ae4b72f00860816cc1caea6ff1a2c91ea1d1968e4dc5fc78517e423510551afc",
|
||||
"resource/global/txwy/resource/template/MiniGame/SPA/MiniGame@SPA@NoMoreHint": "5277b19d824d03014b7c0f185c52743b8913a6808772f00771a983b93f9cfa50",
|
||||
"resource/global/txwy/resource/template/MiniGame/SPA/MiniGame@SPA@EnterIndependent": "c696541cb2ef78485294eeb3245b031601f7d4e8ef8b31113324a2a3ac9ca096",
|
||||
"resource/global/txwy/resource/template/MiniGame/SPA/MiniGame@SPA@StartSimulation": "efb5cd732b62eb7cd509bdff410547d4165b98df03e01fcf2ab6d366d4f89384"
|
||||
"resource/global/txwy/resource/template/MiniGame/SPA/MiniGame@SPA@StartSimulation": "efb5cd732b62eb7cd509bdff410547d4165b98df03e01fcf2ab6d366d4f89384",
|
||||
"resource/template/StageNavigation/SideStory/TD/TD-2": "099e400d7de609929ffe5ee807a6f08d21e20d5a48d8f5f12186547e488b5d64",
|
||||
"resource/template/StageNavigation/SideStory/TD/TD-3": "04a731d18d39314f09238e0df46cc34ac7dcbbcfe68c25f02a8674bec5dad347",
|
||||
"resource/template/StageNavigation/SideStory/TD/TD-P-1": "22d8a61375ae0e66eea90275157a453ae471daf39c1b134cc745394714466fe5",
|
||||
"resource/template/StageNavigation/SideStory/TD/TD-1": "266fa0c2972e5f2d0abc0227df56a4e88c4cede44cef6277829843bf68611b3d",
|
||||
"resource/template/StageNavigation/SideStory/TD/TD-5": "27536f748da64e8d58b4cb793e4b10a494751b5c030deaa20728e8d0e478722a",
|
||||
"resource/template/StageNavigation/SideStory/TD/TD-4": "b195912f24b5c3fc882fd2ac9bb003571f31aa16cf78f604bfb8f21bcbbb5a3f"
|
||||
}
|
||||
Reference in New Issue
Block a user