Compare commits

...

4 Commits

Author SHA1 Message Date
dependabot[bot]
cf570edfc7 ci: bump actions/checkout from 4 to 7 in /.github/workflows in the github-actions group (#17262)
Signed-off-by: dependabot[bot] <support@github.com>
2026-07-04 22:54:42 +08:00
uye
08176f1e72 perf: 持久化储存主题色避免启动时闪烁 (#17263)
* perf: 持久化储存主题色避免启动时闪烁

* perf: 优化深色模式下的勾选框显示效果
2026-07-04 21:35:41 +08:00
github-actions[bot]
2f5b6528cc chore: Auto Templates Optimization
Triggered by a5e468a608

[skip changelog]
2026-07-04 12:02:57 +00:00
uye
a5e468a608 feat: 自动战斗多作业模式支持异体字关卡导航(需更新版本) (#16984)
## Summary by Sourcery

通过基于模板的关卡导航来处理活动关卡与支线关卡,当没有可用模板时回退到 OCR。

新功能:
- 添加一个辅助工具,用标准化关卡代码解析活动关卡模板路径,并在资源存在时使用模板匹配进行关卡导航。
- 为多协助作战关卡选择和支线复刻关卡选择启用基于模板的导航,以提升关卡识别的稳定性。

改进:
- 在资源加载阶段,按完整相对路径索引所有模板图片文件,以便在 C++ 代码中动态查找关卡导航模板。

<details>
<summary>Original summary in English</summary>

## Summary by Sourcery

Introduce template-based stage navigation for event and side-story
stages, falling back to OCR when no template is available.

New Features:
- Add a helper to resolve event stage template paths from standardized
stage codes and use template matching for stage navigation when
resources exist.
- Enable template-based navigation for multi-copilot stage selection and
side-story reopen stage selection to improve stage recognition
reliability.

Enhancements:
- Index all template image files by full relative path during resource
loading to allow dynamic lookup of stage navigation templates from C++
code.

</details>

新功能:
- 为事件关卡引入基于模板的导航机制,通过从标准化路径解析每个关卡的截图模板,并在可用时优先使用模板而不是 OCR。
- 将基于模板的导航应用于多协助器(multi-copilot)关卡选择以及支线故事重开关卡选择,以在这些流程中提供更稳健的关卡识别支持。

增强:
- 在加载模板资源时按相对路径索引所有模板图片文件,以便可以从 C++ 代码中动态查找关卡导航模板。

<details>
<summary>Original summary in English</summary>

## Summary by Sourcery

通过基于模板的关卡导航来处理活动关卡与支线关卡,当没有可用模板时回退到 OCR。

新功能:
- 添加一个辅助工具,用标准化关卡代码解析活动关卡模板路径,并在资源存在时使用模板匹配进行关卡导航。
- 为多协助作战关卡选择和支线复刻关卡选择启用基于模板的导航,以提升关卡识别的稳定性。

改进:
- 在资源加载阶段,按完整相对路径索引所有模板图片文件,以便在 C++ 代码中动态查找关卡导航模板。

<details>
<summary>Original summary in English</summary>

## Summary by Sourcery

Introduce template-based stage navigation for event and side-story
stages, falling back to OCR when no template is available.

New Features:
- Add a helper to resolve event stage template paths from standardized
stage codes and use template matching for stage navigation when
resources exist.
- Enable template-based navigation for multi-copilot stage selection and
side-story reopen stage selection to improve stage recognition
reliability.

Enhancements:
- Index all template image files by full relative path during resource
loading to allow dynamic lookup of stage navigation templates from C++
code.

</details>

</details>
2026-07-04 20:02:34 +08:00
18 changed files with 214 additions and 14 deletions

View File

@@ -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

View File

@@ -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"],

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -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;

View File

@@ -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();

View File

@@ -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, { { "-", "" } });

View File

@@ -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)) { // 关卡介绍已展开

View 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

View File

@@ -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)
{

View File

@@ -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}" />

View File

@@ -361,7 +361,8 @@ public class SettingsViewModel : Screen
GuiSettings.SwitchDarkMode();
// 主题初始化完成后,若莫奈取色已开启,恢复调色板(必须在主题切换之后执行)
BackgroundSettings.UpdateMonet();
// 跳过防抖延迟,避免界面先闪烁原版颜色再显示莫奈主题
BackgroundSettings.UpdateMonet(skipDebounce: true);
}
private void InitConnectConfig()

View File

@@ -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));

View File

@@ -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"
}