Files
better-genshin-impact/BetterGenshinImpact/GameTask/AutoLeyLineOutcrop/AutoLeyLineOutcropTask.cs
火山 8d502d76c1 [优化] 重构万叶战后长E拾取逻辑:提取公共输入时序、增强防卡键保护及OCR释放校验 (#3108)
* 修改万叶的模拟战技与普攻输入操作

将高层的技能释放 / 普通攻击函数调用,替换为明确的模拟输入时序流程,以提升运行稳定性。
改动内容:
AutoFightTask(自动战斗任务)
元素战技采用鼠标 / 按键按下 + 松开时序模拟(长按后松开);将原有三次普通攻击调用,改为6 次鼠标左键按下 / 松开循环,并优化了间隔延时。
AutoLeyLineOutcropTask(自动地脉之花任务)
对万叶长按元素战技(长 E)做同款重构:模拟按键按下 / 松开动作,新增对元素战技冷却区域的视觉 / OCR 识别校验以确认技能已释放;截取冷却区域数据并调用技能后置回调函数,同时沿用 6 次普攻循环;补充了所需的资源引用命名空间。
PickUpCollectHandler(拾取收集处理器)
将长 E 预设等待时长从 1.0 秒 调整为 0.8 秒。
改动说明
本次优化调校了各操作时序,并新增视觉校验机制,减少拾取、技能连招过程中技能 / 普攻输入失效、漏触发的问题。

* Update BetterGenshinImpact/GameTask/AutoFight/AutoFightTask.cs

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update BetterGenshinImpact/GameTask/AutoLeyLineOutcrop/AutoLeyLineOutcropTask.cs

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* 重构万叶战后长E拾取逻辑:提取公共输入时序、增强防卡键保护及OCR释放校验

- 提取公共输入时序方法 ( TaskControl.cs )
- 新增 SimulateHoldActionAsync 、 SimulateHoldElementalSkillAsync :封装了包含前摇处理、精准延时按压和后摇缓冲的长按逻辑。
- 新增 SimulateMouseLeftClickLoopAsync :封装了左键连续点击循环。
- 核心安全提升 :在上述所有涉及 KeyDown/LeftButtonDown 的方法中,全面引入了双层 try/finally 块,确保在任何异常或手动停止任务的情况下,必然触发 KeyUp/LeftButtonUp 。
- 重构自动战斗拾取 ( AutoFightTask.cs )
- 移除 picker.UseSkill(true) ,接入新的公共方法,将长 E 持续时间精准设定为 800ms 。
- 重构地脉拾取并增加状态双重校验 ( AutoLeyLineOutcropTask.cs )
- 接入公共方法,将长 E 持续时间设定为 1000ms 。
- 新增校验拦截 :在松开 E 键后,截取当前画面,通过 HSV过滤 + PaddleOCR 读取技能 CD 数字,结合 Bv.IsSkillReady 进行双重验证。若未读取到 CD 且图标依然高亮(技能未成功释放),则提前 return 跳过后续左键下落攻击动作,并 阻断 AfterUseSkill 的调用,避免污染 CD 池。

* Update TaskControl.cs

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-05-09 10:23:31 +08:00

2748 lines
91 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using BetterGenshinImpact.Core.Recognition;
using BetterGenshinImpact.Core.Recognition.OpenCv;
using BetterGenshinImpact.Core.Recognition.OCR;
using BetterGenshinImpact.Core.Config;
using BetterGenshinImpact.Core.Script;
using BetterGenshinImpact.Core.Simulator;
using BetterGenshinImpact.Core.Simulator.Extensions;
using BetterGenshinImpact.GameTask.AutoDomain;
using BetterGenshinImpact.GameTask.AutoPathing;
using BetterGenshinImpact.GameTask.AutoPathing.Handler;
using BetterGenshinImpact.GameTask.AutoPathing.Model;
using BetterGenshinImpact.GameTask.AutoTrackPath;
using BetterGenshinImpact.GameTask.AutoFight;
using BetterGenshinImpact.GameTask.AutoFight.Assets;
using BetterGenshinImpact.GameTask.AutoPick.Assets;
using BetterGenshinImpact.GameTask.AutoFight.Model;
using BetterGenshinImpact.GameTask.AutoFight.Script;
using BetterGenshinImpact.GameTask.Common;
using BetterGenshinImpact.GameTask.Common.BgiVision;
using BetterGenshinImpact.GameTask.Common.Job;
using BetterGenshinImpact.GameTask.Common.Map.Maps.Base;
using BetterGenshinImpact.GameTask;
using BetterGenshinImpact.GameTask.Model;
using BetterGenshinImpact.GameTask.Model.Area;
using BetterGenshinImpact.Service.Notification;
using BetterGenshinImpact.View.Drawable;
using Microsoft.Extensions.Logging;
using OpenCvSharp;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using BetterGenshinImpact.GameTask.AutoGeniusInvokation.Exception;
using Vanara.PInvoke;
using static BetterGenshinImpact.GameTask.Common.TaskControl;
using BetterGenshinImpact.View;
namespace BetterGenshinImpact.GameTask.AutoLeyLineOutcrop;
public class AutoLeyLineOutcropTask : ISoloTask
{
private readonly ILogger<AutoLeyLineOutcropTask> _logger = App.GetLogger<AutoLeyLineOutcropTask>();
private readonly AutoLeyLineOutcropParam _taskParam;
private readonly bool _oneDragonMode;
private TpTask _tpTask = null!;
private readonly ReturnMainUiTask _returnMainUiTask = new();
private SwitchPartyTask? _switchPartyTask;
private ISystemInfo _systemInfo = null!;
private CancellationToken _ct;
private AutoLeyLineConfigData? _configData;
private NodeData? _nodeData;
private double _leyLineX;
private double _leyLineY;
private int _currentRunTimes;
private bool _marksStatus = true;
private int _recheckCount;
private int _consecutiveFailureCount;
private DateTime _lastRewardNavLog = DateTime.MinValue;
private RecognitionObject? _openRo;
private RecognitionObject? _closeRo;
private RecognitionObject? _paimonMenuRo;
private RecognitionObject? _boxIconRo;
private RecognitionObject? _mapSettingButtonRo;
private RecognitionObject? _handbookTrackActionRo;
private RecognitionObject? _ocrRo1;
private RecognitionObject? _ocrRo2;
private RecognitionObject? _ocrRo3;
private readonly RecognitionObject _ocrRoThis = RecognitionObject.OcrThis;
private readonly Dictionary<string, Mat> _templateCache = new();
private const int MaxRecheckCount = 3;
private const int MaxConsecutiveFailures = 5;
private const string OcrFlowOverlayKey = "AutoLeyLineOutcrop.OcrFlow";
private const string OcrFightOverlayKey = "AutoLeyLineOutcrop.OcrFight";
private const int OcrOverlayRenderLeadMs = 300;
private const int KazuhaPickupPostSkillWaitMs = 3000;
private static readonly TimeSpan LeyLineFightSeekInitialDelay = TimeSpan.FromSeconds(2);
private static readonly Rect HandbookTrackActionButtonRoi = new(ScaleTo1080(1120), ScaleTo1080(680), ScaleTo1080(700), ScaleTo1080(320));
private static readonly System.Drawing.Pen OcrOverlayPen = new(System.Drawing.Color.Lime, 2);
private static readonly object PickLock = new();
private bool _overlayDisplayTemporarilyEnabled;
private bool _overlayDisplayOriginalValue;
private DateTime _lastMaskBringTopTime = DateTime.MinValue;
private bool _friendshipTeamSwitched;
public string Name => "自动地脉花";
public AutoLeyLineOutcropTask(AutoLeyLineOutcropParam taskParam, bool oneDragonMode = false)
{
_taskParam = taskParam;
_oneDragonMode = oneDragonMode;
}
public async Task Start(CancellationToken ct)
{
_ct = ct;
try
{
Initialize();
EnsureMaskOverlayVisible();
var runTimesValue = await HandleResinExhaustionMode();
if (runTimesValue <= 0)
{
throw new Exception("树脂耗尽,任务结束");
}
await PrepareForLeyLineRun();
await RunLeyLineChallenges();
if (_taskParam.IsResinExhaustionMode)
{
await RecheckResinAndContinue();
}
}
catch (Exception e) when (e is NormalEndException or TaskCanceledException)
{
Logger.LogInformation("任务结束:{Msg}", e.Message);
}
catch (Exception e)
{
_logger.LogDebug(e, "自动地脉花执行失败");
_logger.LogError("自动地脉花执行失败:" + e.Message);
if (_taskParam.IsNotification)
{
Notify.Event("AutoLeyLineOutcrop").Error($"任务失败: {e.Message}");
}
throw new Exception($"自动地脉花执行失败: {e.Message}", e);
}
finally
{
try
{
try
{
await EnsureExitRewardPage();
}
catch (Exception ex)
{
_logger.LogDebug(ex, "地脉花结束后尝试退出奖励界面失败");
}
if (!_marksStatus)
{
await OpenCustomMarks();
}
}
finally
{
ClearOcrOverlayKeys();
RestoreMaskOverlayVisible();
}
}
}
private void Initialize()
{
_systemInfo = TaskContext.Instance().SystemInfo;
_tpTask = new TpTask(_ct);
ValidateSettings();
LoadConfigData();
LoadRecognitionObjects();
}
private void ValidateSettings()
{
_taskParam.FightConfig ??= new AutoLeyLineOutcropFightConfig();
if (string.IsNullOrWhiteSpace(_taskParam.LeyLineOutcropType))
{
throw new Exception("地脉花类型未选择");
}
if (_taskParam.LeyLineOutcropType != "启示之花" && _taskParam.LeyLineOutcropType != "藏金之花")
{
throw new Exception("地脉花类型无效,请重新选择");
}
if (string.IsNullOrWhiteSpace(_taskParam.Country))
{
throw new Exception("国家未配置");
}
if (!string.IsNullOrWhiteSpace(_taskParam.FriendshipTeam) && string.IsNullOrWhiteSpace(_taskParam.Team))
{
throw new Exception("配置好感队时必须配置战斗队伍");
}
if (_taskParam.Count < 1)
{
_taskParam.Count = 1;
}
if (string.IsNullOrWhiteSpace(_taskParam.FightConfig.StrategyName) && _taskParam.Timeout > 0)
{
_taskParam.FightConfig.Timeout = _taskParam.Timeout;
}
if (_taskParam.FightConfig.Timeout <= 0)
{
_taskParam.FightConfig.Timeout = _taskParam.Timeout > 0 ? _taskParam.Timeout : 120;
}
else
{
_taskParam.Timeout = _taskParam.FightConfig.Timeout;
}
}
private void LoadConfigData()
{
// Load and validate the static ley line route config from disk.
var workDir = Global.Absolute(@"GameTask\AutoLeyLineOutcrop");
var configPath = Path.Combine(workDir, "Assets", "config.json");
if (!File.Exists(configPath))
{
throw new FileNotFoundException("config.json 未找到", configPath);
}
var json = File.ReadAllText(configPath);
_configData = JsonSerializer.Deserialize<AutoLeyLineConfigData>(json)
?? throw new Exception("config.json 解析失败");
}
private void LoadRecognitionObjects()
{
// Template ROIs are tuned for the 1080p capture region.
_openRo = BuildTemplate("Assets/icon/open.png");
_closeRo = BuildTemplate("Assets/icon/close.png");
_paimonMenuRo = BuildTemplate("Assets/icon/paimon_menu.png", new Rect(0, 0, ScaleTo1080(640), ScaleTo1080(216)));
_boxIconRo = BuildTemplate("Assets/icon/box.png");
_mapSettingButtonRo = BuildTemplate("Assets/icon/map_setting_button.bmp");
_handbookTrackActionRo = BuildTemplate("Assets/icon/handbook_track_action_left.png", HandbookTrackActionButtonRoi, 0.72);
_ocrRo1 = RecognitionObject.Ocr(ScaleTo1080(800), ScaleTo1080(200), ScaleTo1080(300), ScaleTo1080(100));
_ocrRo2 = RecognitionObject.Ocr(ScaleTo1080(0), ScaleTo1080(200), ScaleTo1080(300), ScaleTo1080(300));
_ocrRo3 = RecognitionObject.Ocr(ScaleTo1080(1200), ScaleTo1080(520), ScaleTo1080(300), ScaleTo1080(300));
}
private static int ScaleTo1080(int value)
{
// CaptureToRectArea returns a 1080p region already.
return value;
}
private RecognitionObject BuildTemplate(string relativePath, Rect? roi = null, double threshold = 0.8)
{
// Cache + scale templates to the current asset scale to keep matching stable.
var mat = LoadTemplate(relativePath);
var ro = RecognitionObject.TemplateMatch(mat);
ro.Threshold = threshold;
if (roi.HasValue)
{
ro.RegionOfInterest = roi.Value;
}
return ro;
}
private Mat LoadTemplate(string relativePath)
{
if (_templateCache.TryGetValue(relativePath, out var cached))
{
return cached;
}
var workDir = Global.Absolute(@"GameTask\AutoLeyLineOutcrop");
var fullPath = Path.Combine(workDir, relativePath.Replace("/", Path.DirectorySeparatorChar.ToString()));
if (!File.Exists(fullPath))
{
throw new FileNotFoundException("模板素材未找到", fullPath);
}
var mat = Mat.FromStream(File.OpenRead(fullPath), ImreadModes.Color);
// Resize once and reuse to avoid repeated scaling during recognition.
var scaled = ResizeHelper.Resize(mat, _systemInfo.AssetScale);
_templateCache[relativePath] = scaled;
return scaled;
}
private async Task<int> HandleResinExhaustionMode()
{
if (!_taskParam.IsResinExhaustionMode)
{
return _taskParam.Count;
}
var result = await CalCountByResin();
if (result.Count <= 0)
{
return 0;
}
if (_taskParam.OpenModeCountMin)
{
_taskParam.Count = Math.Min(result.Count, _taskParam.Count);
}
else
{
_taskParam.Count = result.Count;
}
if (_taskParam.IsNotification)
{
var text =
"树脂耗尽模式统计结果:\n" +
$"原粹树脂次数: {result.OriginalResinTimes}\n" +
$"浓缩树脂次数: {result.CondensedResinTimes}\n" +
$"须臾树脂次数: {result.TransientResinTimes}\n" +
$"脆弱树脂次数: {result.FragileResinTimes}\n" +
$"总次数: {result.Count}";
Notify.Event("AutoLeyLineOutcrop").Send(text);
}
return _taskParam.Count;
}
private async Task PrepareForLeyLineRun()
{
await EnsureExitRewardPage();
await _returnMainUiTask.Start(_ct);
if (!_oneDragonMode)
{
await _tpTask.TpToStatueOfTheSeven();
}
if (!string.IsNullOrWhiteSpace(_taskParam.Team))
{
await TrySwitchPartyAndSync(_taskParam.Team);
}
if (_taskParam.UseAdventurerHandbook)
{
// The config flag means "do NOT use handbook"; close custom marks for manual navigation.
await CloseCustomMarks();
}
TaskTriggerDispatcher.Instance().AddTrigger("AutoPick", null);
}
private async Task RunLeyLineChallenges()
{
while (_currentRunTimes < _taskParam.Count)
{
if (!_taskParam.UseAdventurerHandbook)
{
// Handbook flow: open the book and track a ley line target.
await FindLeyLineOutcropByBook(_taskParam.Country, _taskParam.LeyLineOutcropType);
}
else
{
// Manual flow: detect the ley line on the big map.
await FindLeyLineOutcrop(_taskParam.Country, _taskParam.LeyLineOutcropType);
}
var foundStrategy = await ExecuteMatchingStrategy();
if (!foundStrategy)
{
HandleNoStrategyFound();
return;
}
}
}
private async Task<bool> ExecuteMatchingStrategy()
{
if (_configData?.LeyLinePositions == null)
{
throw new Exception("地脉花策略配置缺失");
}
if (!_configData.LeyLinePositions.TryGetValue(_taskParam.Country, out var positions))
{
return false;
}
foreach (var position in positions)
{
if (IsNearPosition(_leyLineX, _leyLineY, position.X, position.Y, _configData.ErrorThreshold))
{
_logger.LogInformation("匹配策略: {Strategy} order={Order}", position.Strategy, position.Order);
await ExecutePathsUsingNodeData(position);
return true;
}
}
return false;
}
private static bool IsNearPosition(double x1, double y1, double x2, double y2, double threshold)
{
return Math.Abs(x1 - x2) <= threshold && Math.Abs(y1 - y2) <= threshold;
}
private async Task ExecutePathsUsingNodeData(LeyLinePosition position)
{
try
{
// Map node graph provides the walking routes for each ley line position.
var nodeData = await LoadNodeData();
var targetNode = FindTargetNodeByPosition(nodeData, position.X, position.Y);
if (targetNode == null)
{
await EnsureExitRewardPage();
return;
}
var paths = FindPathsToTarget(nodeData, targetNode);
if (paths.Count == 0)
{
await EnsureExitRewardPage();
return;
}
var optimal = SelectOptimalPath(paths);
await ExecutePath(optimal);
_currentRunTimes++;
if (_currentRunTimes >= _taskParam.Count)
{
return;
}
var currentNode = targetNode;
while (currentNode.Next.Count > 0 && _currentRunTimes < _taskParam.Count)
{
if (currentNode.Next.Count == 1)
{
var next = currentNode.Next[0];
var nextNode = nodeData.Nodes.FirstOrDefault(n => n.Id == next.Target);
if (nextNode == null)
{
await EnsureExitRewardPage();
return;
}
var path = new PathInfo
{
StartNode = currentNode,
TargetNode = nextNode,
Routes = [next.Route]
};
await ExecutePath(path);
_currentRunTimes++;
currentNode = nextNode;
}
else
{
// Multiple branches: re-locate the ley line position before deciding the route.
var originalX = _leyLineX;
var originalY = _leyLineY;
await _returnMainUiTask.Start(_ct);
await _tpTask.OpenBigMapUi();
var found = await LocateLeyLineOutcrop(_taskParam.LeyLineOutcropType);
await _returnMainUiTask.Start(_ct);
if (!found)
{
_leyLineX = originalX;
_leyLineY = originalY;
await EnsureExitRewardPage();
return;
}
var selected = SelectBranchRoute(nodeData, currentNode);
if (selected == null)
{
_leyLineX = originalX;
_leyLineY = originalY;
await EnsureExitRewardPage();
return;
}
var path = new PathInfo
{
StartNode = currentNode,
TargetNode = selected.Value.Node,
Routes = [selected.Value.Route]
};
await ExecutePath(path);
_currentRunTimes++;
currentNode = selected.Value.Node;
}
}
}
catch (Exception ex)
{
if (ex.Message.Contains("战斗失败", StringComparison.OrdinalIgnoreCase))
{
_consecutiveFailureCount++;
if (_consecutiveFailureCount >= MaxConsecutiveFailures)
{
await EnsureExitRewardPage();
throw new Exception($"连续战斗失败{MaxConsecutiveFailures}次,任务终止");
}
await EnsureExitRewardPage();
_logger.LogInformation("战斗失败,重新寻找地脉花");
return;
}
await EnsureExitRewardPage();
throw;
}
}
private (string Route, Node Node)? SelectBranchRoute(NodeData nodeData, Node currentNode)
{
string? selectedRoute = null;
Node? selectedNode = null;
var closest = double.MaxValue;
foreach (var next in currentNode.Next)
{
var branchNode = nodeData.Nodes.FirstOrDefault(n => n.Id == next.Target);
if (branchNode == null)
{
continue;
}
var distance = Calculate2DDistance(_leyLineX, _leyLineY, branchNode.Position.X, branchNode.Position.Y);
if (distance < closest)
{
closest = distance;
selectedRoute = next.Route;
selectedNode = branchNode;
}
}
if (selectedRoute == null || selectedNode == null)
{
return null;
}
return (selectedRoute, selectedNode);
}
private static double Calculate2DDistance(double x1, double y1, double x2, double y2)
{
var dx = x1 - x2;
var dy = y1 - y2;
return Math.Sqrt(dx * dx + dy * dy);
}
private Node? FindTargetNodeByPosition(NodeData nodeData, double x, double y)
{
const double errorThreshold = 50;
return nodeData.Nodes.FirstOrDefault(node =>
node.Type == "blossom" &&
Math.Abs(node.Position.X - x) <= errorThreshold &&
Math.Abs(node.Position.Y - y) <= errorThreshold);
}
private List<PathInfo> FindPathsToTarget(NodeData nodeData, Node targetNode)
{
return BreadthFirstPathSearch(nodeData, targetNode);
}
private List<PathInfo> BreadthFirstPathSearch(NodeData nodeData, Node targetNode)
{
var validPaths = new List<PathInfo>();
var teleportNodes = nodeData.Nodes.Where(n => n.Type == "teleport").ToList();
var nodeMap = nodeData.Nodes.ToDictionary(n => n.Id, n => n);
foreach (var startNode in teleportNodes)
{
var queue = new Queue<(Node Node, PathInfo Path, HashSet<int> Visited)>();
// BFS ensures we prefer shorter paths from each teleport node.
queue.Enqueue((startNode, new PathInfo
{
StartNode = startNode,
TargetNode = targetNode,
Routes = new List<string>()
}, new HashSet<int> { startNode.Id }));
while (queue.Count > 0)
{
var (current, path, visited) = queue.Dequeue();
if (current.Id == targetNode.Id)
{
validPaths.Add(path);
continue;
}
foreach (var next in current.Next)
{
if (visited.Contains(next.Target))
{
continue;
}
if (!nodeMap.TryGetValue(next.Target, out var nextNode))
{
continue;
}
var newRoutes = new List<string>(path.Routes) { next.Route };
var newVisited = new HashSet<int>(visited) { next.Target };
queue.Enqueue((nextNode, new PathInfo
{
StartNode = path.StartNode,
TargetNode = targetNode,
Routes = newRoutes
}, newVisited));
}
}
}
validPaths.AddRange(FindReversePathsIfNeeded(nodeData, targetNode, validPaths));
return validPaths;
}
private static List<PathInfo> FindReversePathsIfNeeded(NodeData nodeData, Node targetNode, List<PathInfo> existingPaths)
{
if (existingPaths.Count > 0 || targetNode.Prev.Count == 0)
{
return [];
}
// Fallback: allow a single hop into the target when no forward path exists.
var reversePaths = new List<PathInfo>();
var nodeMap = nodeData.Nodes.ToDictionary(n => n.Id, n => n);
foreach (var prevNodeId in targetNode.Prev)
{
if (!nodeMap.TryGetValue(prevNodeId, out var prevNode))
{
continue;
}
var teleportNodes = nodeData.Nodes.Where(node =>
node.Type == "teleport" && node.Next.Any(route => route.Target == prevNode.Id)).ToList();
foreach (var teleportNode in teleportNodes)
{
var route = teleportNode.Next.FirstOrDefault(r => r.Target == prevNode.Id);
var nextRoute = prevNode.Next.FirstOrDefault(r => r.Target == targetNode.Id);
if (route == null || nextRoute == null)
{
continue;
}
reversePaths.Add(new PathInfo
{
StartNode = teleportNode,
TargetNode = targetNode,
Routes = [route.Route, nextRoute.Route]
});
}
}
return reversePaths;
}
private static PathInfo SelectOptimalPath(List<PathInfo> paths)
{
if (paths.Count == 0)
{
throw new Exception("没有可用路径");
}
return paths.OrderBy(p => p.Routes.Count).First();
}
private async Task ExecutePath(PathInfo path)
{
foreach (var routePath in path.Routes)
{
await RunPathingFile(routePath);
}
var lastRoute = path.Routes.Last();
var targetRoute = BuildTargetRoute(lastRoute);
var rerunRoute = BuildRerunRoute(lastRoute);
var fromTeleportStart = "teleport".Equals(path.StartNode.Type, StringComparison.OrdinalIgnoreCase);
await ProcessLeyLineOutcrop(_taskParam.FightConfig.Timeout, targetRoute, rerunRoute, fromTeleportStart);
var rewardSuccess = await AttemptReward();
if (!rewardSuccess)
{
throw new Exception("无法领取奖励");
}
await TryScanDropsAfterReward();
_consecutiveFailureCount = 0;
}
private static string BuildTargetRoute(string routePath)
{
return routePath
.Replace("assets/pathing/", "assets/pathing/target/", StringComparison.OrdinalIgnoreCase)
.Replace("-rerun", "", StringComparison.OrdinalIgnoreCase);
}
private static string BuildRerunRoute(string routePath)
{
var rerunRoute = routePath
.Replace("assets/pathing/target/", "assets/pathing/rerun/", StringComparison.OrdinalIgnoreCase)
.Replace("assets/pathing/", "assets/pathing/rerun/", StringComparison.OrdinalIgnoreCase);
if (!rerunRoute.Contains("-rerun", StringComparison.OrdinalIgnoreCase))
{
rerunRoute = rerunRoute.Replace(".json", "-rerun.json", StringComparison.OrdinalIgnoreCase);
}
return rerunRoute;
}
private bool PathingFileExists(string routePath)
{
var workDir = Global.Absolute(@"GameTask\AutoLeyLineOutcrop");
var localPath = routePath.Replace("/", Path.DirectorySeparatorChar.ToString());
var fullPath = Path.Combine(workDir, localPath);
return File.Exists(fullPath);
}
private async Task RunPathingFile(string routePath)
{
await _returnMainUiTask.Start(_ct);
var workDir = Global.Absolute(@"GameTask\AutoLeyLineOutcrop");
var localPath = routePath.Replace("/", Path.DirectorySeparatorChar.ToString());
var fullPath = Path.Combine(workDir, localPath);
var task = PathingTask.BuildFromFilePath(fullPath) ?? throw new Exception("路径文件解析失败");
var executor = new PathExecutor(_ct);
executor.PartyConfig = BuildLeyLinePathingPartyConfig();
await executor.Pathing(task);
}
private static PathingPartyConfig BuildLeyLinePathingPartyConfig()
{
var partyConfig = PathingPartyConfig.BuildDefault();
partyConfig.SkipPartySwitch = true;
return partyConfig;
}
private async Task<NodeData> LoadNodeData()
{
if (_nodeData != null)
{
return _nodeData;
}
var workDir = Global.Absolute(@"GameTask\AutoLeyLineOutcrop");
var nodePath = Path.Combine(workDir, "Assets", "LeyLineOutcropData.json");
if (!File.Exists(nodePath))
{
throw new FileNotFoundException("LeyLineOutcropData.json 未找到", nodePath);
}
var raw = JsonSerializer.Deserialize<RawNodeData>(File.ReadAllText(nodePath))
?? throw new Exception("节点数据解析失败");
_nodeData = AdaptNodeData(raw);
return _nodeData;
}
private static NodeData AdaptNodeData(RawNodeData raw)
{
var nodes = new List<Node>();
foreach (var teleport in raw.Teleports)
{
nodes.Add(new Node
{
Id = teleport.Id,
Region = teleport.Region,
Position = teleport.Position,
Type = "teleport",
Next = new List<NodeRoute>(),
Prev = new List<int>()
});
}
foreach (var blossom in raw.Blossoms)
{
nodes.Add(new Node
{
Id = blossom.Id,
Region = blossom.Region,
Position = blossom.Position,
Type = "blossom",
Next = new List<NodeRoute>(),
Prev = new List<int>()
});
}
foreach (var edge in raw.Edges)
{
var sourceNode = nodes.FirstOrDefault(n => n.Id == edge.Source);
var targetNode = nodes.FirstOrDefault(n => n.Id == edge.Target);
if (sourceNode == null || targetNode == null)
{
continue;
}
sourceNode.Next.Add(new NodeRoute { Target = edge.Target, Route = edge.Route });
targetNode.Prev.Add(edge.Source);
}
return new NodeData
{
Nodes = nodes,
Indexes = raw.Indexes
};
}
private async Task FindLeyLineOutcrop(string country, string type)
{
if (_configData?.MapPositions == null)
{
throw new Exception("地图位置配置缺失");
}
if (!_configData.MapPositions.TryGetValue(country, out var positions) || positions.Count == 0)
{
throw new Exception($"未找到国家 {country} 的位置信息");
}
await _returnMainUiTask.Start(_ct);
await _tpTask.OpenBigMapUi();
await _tpTask.MoveMapTo(positions[0].X, positions[0].Y, MapTypes.Teyvat.ToString());
var found = await LocateLeyLineOutcrop(type);
if (found)
{
return;
}
for (var i = 1; i < positions.Count; i++)
{
var pos = positions[i];
_logger.LogInformation("尝试定位地脉花: {Name}", pos.Name ?? $"{pos.X},{pos.Y}");
await _tpTask.MoveMapTo(pos.X, pos.Y, MapTypes.Teyvat.ToString());
if (await LocateLeyLineOutcrop(type))
{
return;
}
}
await EnsureExitRewardPage();
if (_taskParam.UseAdventurerHandbook)
{
_logger.LogWarning("寻找地脉花失败:当前已勾选“不使用冒险之证寻路”,可尝试关闭该选项后重试!");
throw new Exception("寻找地脉花失败:未在地图上识别到地脉花图标。当前已勾选“不使用冒险之证寻路”,可尝试关闭该选项后重试!");
}
throw new Exception("寻找地脉花失败:未在地图上识别到地脉花图标");
}
private async Task<bool> LocateLeyLineOutcrop(string type)
{
await Delay(500, _ct);
var currentZoom = _tpTask.GetBigMapZoomLevel(CaptureToRectArea());
await _tpTask.AdjustMapZoomLevel(currentZoom, 3.0);
var iconPath = type == "启示之花"
? "Assets/icon/Blossom_of_Revelation.png"
: "Assets/icon/Blossom_of_Wealth.png";
using var ra = CaptureToRectArea();
var iconRo = BuildTemplate(iconPath);
var list = ra.FindMulti(iconRo);
if (list.Count == 0)
{
return false;
}
var flower = list[0];
var center = _tpTask.GetBigMapCenterPoint(MapTypes.Teyvat.ToString());
var mapZoomLevel = _tpTask.GetBigMapZoomLevel(CaptureToRectArea());
var mapScaleFactor = TaskContext.Instance().Config.TpConfig.MapScaleFactor;
_leyLineX = (960 - flower.X - 25) * mapZoomLevel / mapScaleFactor + center.X;
_leyLineY = (540 - flower.Y - 25) * mapZoomLevel / mapScaleFactor + center.Y;
return true;
}
private void HandleNoStrategyFound()
{
_logger.LogError("未找到对应的地脉花策略");
if (_taskParam.IsNotification)
{
Notify.Event("AutoLeyLineOutcrop").Error("未找到对应的地脉花策略");
}
}
private async Task<bool> ProcessLeyLineOutcrop(int timeoutSeconds, string targetPath, string rerunPath, bool fromTeleportStart, int retries = 0)
{
const int maxRetries = 3;
if (retries >= maxRetries)
{
await EnsureExitRewardPage();
throw new Exception("开启地脉花失败,已达最大重试次数");
}
await Delay(500, _ct);
_logger.LogDebug("检测地脉花交互状态,重试次数: {Retries}/{MaxRetries}", retries + 1, maxRetries);
using var capture = CaptureToRectArea();
using var ocrOverlayScope = DrawOcrOverlayScope(capture, OcrFlowOverlayKey, _ocrRo2!.RegionOfInterest, _ocrRo3!.RegionOfInterest);
await WaitOcrOverlayRenderTick();
string result1Text;
string result2Text;
var result1 = FindSafe(capture, _ocrRo2!);
var result2 = FindSafe(capture, _ocrRo3!);
result1Text = result1.Text;
result2Text = result2.Text;
_logger.LogDebug("OCR结果: result1='{Text1}', result2='{Text2}'", result1Text, result2Text);
if (IsLeyLineRewardReadyState(capture, result1Text, result2Text))
{
_logger.LogDebug("识别到地脉花领奖状态");
await SwitchToFriendshipTeamIfNeeded();
return true;
}
if (ContainsLeyLineFlowerText(result2Text))
{
_logger.LogDebug("识别到地脉之花入口,尝试接触");
Simulation.SendInput.SimulateAction(GIActions.PickUpOrInteract);
await Delay(800, _ct);
using var postInteractCapture = CaptureToRectArea();
result1 = FindSafe(postInteractCapture, _ocrRo2!);
result2 = FindSafe(postInteractCapture, _ocrRo3!);
result1Text = result1.Text;
result2Text = result2.Text;
_logger.LogDebug("接触后OCR结果: result1='{Text1}', result2='{Text2}'", result1Text, result2Text);
if (IsLeyLineRewardReadyState(postInteractCapture, result1Text, result2Text))
{
_logger.LogDebug("接触后识别到地脉花领奖状态");
await SwitchToFriendshipTeamIfNeeded();
return true;
}
}
if (result2Text.Contains("溢口", StringComparison.Ordinal))
{
_logger.LogDebug("识别到溢口提示,尝试交互");
Simulation.SendInput.SimulateAction(GIActions.PickUpOrInteract);
await Delay(300, _ct);
Simulation.SendInput.SimulateAction(GIActions.PickUpOrInteract);
await Delay(500, _ct);
}
else if (!ContainsFightText(result1Text))
{
var recoverPath = retries == 0
? targetPath
: fromTeleportStart
? targetPath
: rerunPath;
if (!PathingFileExists(recoverPath))
{
_logger.LogWarning("纠偏路径不存在回退target路径: {Path}", recoverPath);
recoverPath = targetPath;
}
_logger.LogDebug("未识别到战斗提示,执行纠偏路径: {Path}", recoverPath);
await RunPathingFile(recoverPath);
return await ProcessLeyLineOutcrop(timeoutSeconds, targetPath, rerunPath, fromTeleportStart, retries + 1);
}
var fightResult = await AutoFight(timeoutSeconds);
if (!fightResult)
{
await EnsureExitRewardPage();
if (await ProcessResurrect())
{
return await ProcessLeyLineOutcrop(timeoutSeconds, targetPath, rerunPath, fromTeleportStart, retries + 1);
}
throw new Exception("战斗失败");
}
await TryCollectDropsAfterFight();
await SwitchToFriendshipTeamIfNeeded();
await AutoNavigateToReward();
return true;
}
private async Task TryCollectDropsAfterFight()
{
if (!_taskParam.FightConfig.KazuhaPickupEnabled)
{
return;
}
try
{
var combatScenes = await RunnerContext.Instance.GetCombatScenes(_ct);
if (combatScenes == null)
{
_logger.LogWarning("战后聚集拾取:队伍识别失败,跳过");
return;
}
var kazuha = combatScenes.SelectAvatar("枫原万叶");
if (kazuha != null)
{
await TryKazuhaCollect(kazuha);
return;
}
var qin = combatScenes.SelectAvatar("琴");
if (qin != null)
{
await TryQinCollect(combatScenes, qin);
return;
}
_logger.LogDebug("战后聚集拾取:当前队伍无万叶/琴,跳过");
}
catch (Exception ex)
{
_logger.LogDebug(ex, "战后聚集拾取异常");
}
finally
{
Simulation.ReleaseAllKey();
}
}
/// <summary>
/// 在地脉花奖励领取完成后,短时间扫描周围掉落物光柱并尝试靠近拾取。
/// </summary>
private async Task TryScanDropsAfterReward()
{
if (!_taskParam.ScanDropsAfterRewardEnabled)
{
return;
}
const int maxScanSeconds = 60;
var scanSeconds = Math.Clamp(_taskParam.ScanDropsAfterRewardSeconds, 0, maxScanSeconds);
if (scanSeconds <= 0)
{
return;
}
var autoFightConfig = TaskContext.Instance().Config.AutoFightConfig;
var originalSeconds = autoFightConfig.PickDropsAfterFightSeconds;
try
{
autoFightConfig.PickDropsAfterFightSeconds = scanSeconds;
_logger.LogInformation("领取奖励后开始扫描掉落物光柱,时长 {Seconds} 秒", scanSeconds);
await new ScanPickTask().Start(_ct);
}
catch (Exception ex) when (ex is OperationCanceledException or TaskCanceledException)
{
throw;
}
catch (Exception ex)
{
_logger.LogDebug(ex, "领取奖励后扫描掉落物光柱异常");
}
finally
{
autoFightConfig.PickDropsAfterFightSeconds = originalSeconds;
Simulation.ReleaseAllKey();
}
}
private async Task TryKazuhaCollect(Avatar kazuha)
{
_logger.LogInformation("战后聚集拾取开始使用枫原万叶执行长E拾取");
await Delay(200, _ct);
if (!kazuha.TrySwitch(10))
{
_logger.LogWarning("战后聚集拾取:切换到万叶失败,跳过");
return;
}
_logger.LogInformation("战后聚集拾取万叶已切换等待元素战技CD");
await kazuha.WaitSkillCd(_ct);
await SimulateHoldElementalSkillAsync(1000, _ct);
await Delay(200, _ct);
// 获取游戏画面,进行 OCR 及视觉状态双重验证以确认长E技能是否真正释放成功
using (var region = CaptureToRectArea())
{
// 裁剪技能 CD 区域并做 HSV 颜色过滤,分离出白色的 CD 数字
using var eRa = region.DeriveCrop(AutoFightAssets.Instance.ECooldownRect);
using var eRaWhite = OpenCvCommonHelper.InRangeHsv(eRa.SrcMat, new Scalar(0, 0, 235), new Scalar(0, 25, 255));
var text = OcrFactory.Paddle.OcrWithoutDetector(eRaWhite);
// 如果成功读到了大于 0 的 CD 数值,说明技能已释放
var hasOcrCd = double.TryParse(text, out var ocrCd) && ocrCd > 0;
// 视觉上判断当前技能图标是否高亮就绪如果不亮false也说明技能释放进入了冷却
var isVisualReady = Bv.IsSkillReady(region, kazuha.Index, false);
// 当 OCR 没读出 CD可能网络卡顿技能没放出来并且视觉上技能图标依然亮着就绪时判断为释放失败
if (!hasOcrCd && isVisualReady)
{
_logger.LogWarning("战后聚集拾取万叶长E释放确认失败OCR{Text}),跳过后续拾取动作", text);
return;
}
// 更新技能冷却记录,防止干扰后续冷却判断
kazuha.AfterUseSkill(region);
}
await SimulateMouseLeftClickLoopAsync(6, _ct);
await Delay(1500, _ct);
_logger.LogInformation("战后聚集拾取万叶长E动作完成等待拾取动作结束");
await Delay(KazuhaPickupPostSkillWaitMs, _ct);
_logger.LogInformation("战后聚集拾取万叶长E聚集动作执行完成");
}
private async Task TryQinCollect(CombatScenes combatScenes, Avatar qin)
{
_logger.LogInformation("战后聚集拾取:使用琴-长E拾取掉落物");
var find = _taskParam.FightConfig.QinDoublePickUp;
await Delay(150, _ct);
if (qin.TrySwitch(10))
{
var actionsToUse = PickUpCollectHandler.PickUpActions
.Where(action => action.StartsWith("琴-长E ", StringComparison.OrdinalIgnoreCase))
.Select(action => action.Replace("琴-长E", "琴", StringComparison.OrdinalIgnoreCase))
.ToArray();
foreach (var actionStr in actionsToUse)
{
var pickUpAction = CombatScriptParser.ParseContext(actionStr);
for (var i = 0; i < 2; i++)
{
await qin.WaitSkillCd(_ct);
foreach (var command in pickUpAction.CombatCommands)
{
command.Execute(combatScenes);
Task.Run(() =>
{
if (Monitor.TryEnter(PickLock))
{
try
{
if (find)
{
using var imagePick = CaptureToRectArea();
if (imagePick.Find(AutoPickAssets.Instance.PickRo).IsExist())
{
find = false;
}
}
}
finally
{
Monitor.Exit(PickLock);
}
}
});
}
if (!find)
{
break;
}
if (i == 0)
{
_logger.LogInformation("战后聚集拾取:尝试再次执行琴-长E拾取");
qin.AfterUseSkill();
}
else
{
break;
}
}
Simulation.ReleaseAllKey();
}
}
else
{
_logger.LogWarning("战后聚集拾取:切换到琴失败,跳过");
}
}
private Region FindSafe(ImageRegion capture, RecognitionObject ro)
{
var roi = ro.RegionOfInterest;
if (roi == default)
{
return capture.Find(ro);
}
var clamped = roi.ClampTo(capture.Width, capture.Height);
if (clamped.Width <= 0 || clamped.Height <= 0)
{
return new Region();
}
if (clamped == roi)
{
return capture.Find(ro);
}
var cloned = ro.Clone();
cloned.RegionOfInterest = clamped;
return capture.Find(cloned);
}
private async Task<bool> AutoFight(int timeoutSeconds)
{
var fightCts = CancellationTokenSource.CreateLinkedTokenSource(_ct);
using var autoFightConfigScope = UseLeyLineAutoFightConfigScope();
// 地脉花战斗拆成两条并行链路:
// 1. AutoFightTask 持续执行战斗脚本,不负责任何结束判定。
// 2. RecognizeTextInRegion 只通过 OCR 判定胜负,并在轮询间隙补一次内部寻敌辅助。
var fightTask = StartAutoFightWithoutFinishDetect(fightCts.Token);
var fightResult = await RecognizeTextInRegion(timeoutSeconds * 1000);
fightCts.Cancel();
try
{
await fightTask;
}
catch (Exception ex) when (ex is NormalEndException or TaskCanceledException)
{
}
catch (Exception ex)
{
_logger.LogDebug(ex, "自动战斗任务结束");
}
finally
{
Simulation.ReleaseAllKey();
}
return fightResult;
}
private Task StartAutoFightWithoutFinishDetect(CancellationToken ct)
{
var autoFightConfig = BuildLeyLineAutoFightConfig();
var strategyPath = BuildAutoFightStrategyPath(autoFightConfig);
var taskParam = new AutoFightParam(strategyPath, autoFightConfig)
{
FightFinishDetectEnabled = false,
CheckBeforeBurst = false
};
// Avoid false finish signals for ley line fights.
taskParam.FinishDetectConfig.FastCheckEnabled = false;
taskParam.FinishDetectConfig.RotateFindEnemyEnabled = false;
taskParam.PickDropsAfterFightEnabled = false;
taskParam.KazuhaPickupEnabled = false;
taskParam.QinDoublePickUp = false;
taskParam.OnlyPickEliteDropsMode = "DisableAutoPickupForNonElite";
return new AutoFightTask(taskParam).Start(ct);
}
private IDisposable UseLeyLineAutoFightConfigScope()
{
var allConfig = TaskContext.Instance().Config;
var original = allConfig.AutoFightConfig;
allConfig.AutoFightConfig = BuildLeyLineAutoFightConfig();
return new AutoFightConfigScope(allConfig, original);
}
private AutoFightConfig BuildLeyLineAutoFightConfig()
{
var globalAutoFightConfig = TaskContext.Instance().Config.AutoFightConfig;
var fightConfig = _taskParam.FightConfig;
if (string.IsNullOrWhiteSpace(fightConfig.StrategyName))
{
fightConfig.CopyFromAutoFightConfig(globalAutoFightConfig);
}
if (fightConfig.Timeout <= 0)
{
fightConfig.Timeout = _taskParam.Timeout > 0 ? _taskParam.Timeout : Math.Max(globalAutoFightConfig.Timeout, 1);
}
_taskParam.Timeout = fightConfig.Timeout;
return fightConfig.ToAutoFightConfig();
}
private static string BuildAutoFightStrategyPath(AutoFightConfig config)
{
var path = Global.Absolute(@"User\AutoFight\" + config.StrategyName + ".txt");
if ("根据队伍自动选择".Equals(config.StrategyName))
{
path = Global.Absolute(@"User\AutoFight\");
}
if (!File.Exists(path) && !Directory.Exists(path))
{
throw new Exception("战斗策略文件不存在");
}
return path;
}
private async Task<bool> RecognizeTextInRegion(int timeoutMs)
{
var start = DateTime.UtcNow;
var noTextCount = 0;
var fightTextDetectedAt = DateTime.MinValue;
var lastSeekAssistAt = DateTime.MinValue;
var seekEnemyEnabled = _taskParam.FightConfig.SeekEnemyEnabled;
var seekEnemyInterval = TimeSpan.FromSeconds(Math.Clamp(_taskParam.FightConfig.SeekEnemyIntervalSeconds, 1, 60));
var seekEnemyRotaryFactor = Math.Clamp(_taskParam.FightConfig.SeekEnemyRotaryFactor, 1, 13);
var successKeywords = new[] { "挑战达成", "战斗胜利", "挑战成功" };
var failureKeywords = new[] { "挑战失败" };
while ((DateTime.UtcNow - start).TotalMilliseconds < timeoutMs)
{
string text;
bool foundText;
using (var capture = CaptureToRectArea())
using (var ocrOverlayScope = DrawOcrOverlayScope(capture, OcrFightOverlayKey, _ocrRo1!.RegionOfInterest, _ocrRo2!.RegionOfInterest))
{
await WaitOcrOverlayRenderTick();
var result = capture.Find(_ocrRo1!);
text = result.Text;
foundText = RecognizeFightText(capture);
}
if (successKeywords.Any(text.Contains))
{
// OCR recognizes victory text; treat as success.
return true;
}
if (failureKeywords.Any(text.Contains))
{
// OCR recognizes failure text; stop early.
return false;
}
if (!foundText)
{
noTextCount++;
if (noTextCount >= 10)
{
return false;
}
}
else
{
var now = DateTime.UtcNow;
if (fightTextDetectedAt == DateTime.MinValue)
{
fightTextDetectedAt = now;
}
noTextCount = 0;
if (ShouldRunLeyLineFightSeek(seekEnemyEnabled, now, fightTextDetectedAt, lastSeekAssistAt, seekEnemyInterval))
{
lastSeekAssistAt = now;
await TrySeekEnemyDuringLeyLineFight(seekEnemyRotaryFactor);
}
}
await Delay(1000, _ct);
}
return false;
}
private static bool ShouldRunLeyLineFightSeek(bool seekEnemyEnabled, DateTime now, DateTime fightTextDetectedAt, DateTime lastSeekAssistAt, TimeSpan seekEnemyInterval)
{
if (!seekEnemyEnabled)
{
return false;
}
if (fightTextDetectedAt == DateTime.MinValue)
{
return false;
}
if (now - fightTextDetectedAt < LeyLineFightSeekInitialDelay)
{
return false;
}
return lastSeekAssistAt == DateTime.MinValue || now - lastSeekAssistAt >= seekEnemyInterval;
}
private async Task TrySeekEnemyDuringLeyLineFight(int rotaryFactor)
{
try
{
// isEndCheck=true prevents AutoFightSeek from falling back to any party-screen finish check.
await AutoFightSeek.SeekAndFightAsync(_logger, 0, 0, _ct, true, rotaryFactor);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
_logger.LogDebug(ex, "地脉花战斗中内部寻敌异常");
}
}
private bool RecognizeFightText(ImageRegion captureRegion)
{
var result = captureRegion.Find(_ocrRo2!);
var text = result.Text;
return ContainsFightText(text);
}
private bool IsLeyLineRewardReadyState(ImageRegion capture, string result1Text, string result2Text)
{
if (ContainsFightText(result1Text))
{
return false;
}
return ContainsRewardPromptActionText(result2Text) || HasRewardPrompt(capture);
}
private static bool ContainsFightText(string text)
{
text = NormalizeLeyLineOcrText(text);
var keywords = new[] { "打倒", "所有", "敌人" };
return keywords.Any(text.Contains);
}
private async Task AutoNavigateToReward()
{
const int maxRetry = 3;
for (var retry = 0; retry < maxRetry; retry++)
{
// Reset camera and move in short bursts to re-acquire the chest icon.
_logger.LogInformation("开始导航到地脉花奖励,尝试 {Retry}/{Max}", retry + 1, maxRetry);
Simulation.SendInput.Mouse.MiddleButtonClick();
await Delay(300, _ct);
if (await NavigateTowardReward(60000))
{
_logger.LogInformation("已到达领取奖励页面");
return;
}
Simulation.SendInput.SimulateAction(GIActions.MoveForward, KeyType.KeyUp);
Simulation.SendInput.Keyboard.KeyPress(User32.VK.VK_X);
await Delay(500, _ct);
Simulation.SendInput.SimulateAction(GIActions.MoveBackward, KeyType.KeyDown);
await Delay(1000, _ct);
Simulation.SendInput.SimulateAction(GIActions.MoveBackward, KeyType.KeyUp);
await Delay(500, _ct);
}
throw new Exception("导航到地脉花失败:超时未检测到奖励或交互文字");
}
private async Task<bool> NavigateTowardReward(int timeoutMs)
{
var start = DateTime.UtcNow;
try
{
while ((DateTime.UtcNow - start).TotalMilliseconds < timeoutMs)
{
// If reward UI is detected, stop moving.
if (await DetectRewardPage())
{
_logger.LogInformation("检测到奖励/交互文字,停止导航");
return true;
}
using var capture = CaptureToRectArea();
if (_paimonMenuRo != null && capture.Find(_paimonMenuRo).IsEmpty())
{
LogRewardNav("误入其他界面,尝试返回主界面");
await _returnMainUiTask.Start(_ct);
}
if (!AdjustViewForReward(capture))
{
// Wait for the icon to re-enter view before moving forward.
LogRewardNav("未对正地脉花图标,等待重新定位");
Simulation.SendInput.SimulateAction(GIActions.MoveForward, KeyType.KeyUp);
await Delay(1000, _ct);
continue;
}
LogRewardNav("地脉花图标已对正,开始前进");
Simulation.SendInput.SimulateAction(GIActions.MoveForward, KeyType.KeyDown);
await Delay(200, _ct);
}
}
finally
{
Simulation.SendInput.SimulateAction(GIActions.MoveForward, KeyType.KeyUp);
}
return false;
}
private bool AdjustViewForReward(ImageRegion capture)
{
if (_boxIconRo == null)
{
return false;
}
// Use the chest icon position to align the camera before moving forward.
var iconRes = capture.Find(_boxIconRo);
if (iconRes.IsEmpty())
{
LogRewardNav("未找到地脉花图标");
return false;
}
const int screenCenterX = 960;
const int screenCenterY = 540;
const double maxAngle = 10;
var xOffset = iconRes.X - screenCenterX;
var yOffset = screenCenterY - iconRes.Y;
var angleInRadians = Math.Atan2(Math.Abs(xOffset), yOffset);
var angleInDegrees = angleInRadians * (180 / Math.PI);
var isAboveCenter = iconRes.Y < screenCenterY;
var isWithinAngle = angleInDegrees <= maxAngle;
if (isAboveCenter && isWithinAngle)
{
LogRewardNav("地脉花图标已对正,角度: {Angle}", angleInDegrees);
return true;
}
Simulation.SendInput.SimulateAction(GIActions.MoveForward, KeyType.KeyUp);
var moveX = Math.Clamp(xOffset, -300, 300);
LogRewardNav("调整视角xOffset={XOffset}, yOffset={YOffset}, angle={Angle}", xOffset, yOffset, angleInDegrees);
Simulation.SendInput.Mouse.MoveMouseBy(moveX, 0);
if (!isAboveCenter)
{
Simulation.SendInput.Mouse.MoveMouseBy(0, 500);
}
return false;
}
private async Task<bool> DetectRewardPage()
{
using var capture = CaptureToRectArea();
// Bv.FindF is faster for common keywords and avoids OCR misses.
if (Bv.FindF(capture, "接触") || Bv.FindF(capture, "地脉") || Bv.FindF(capture, "之花"))
{
return true;
}
var list = capture.FindMulti(_ocrRoThis);
foreach (var res in list)
{
if (res.Text.Contains("原粹树脂", StringComparison.Ordinal))
{
return true;
}
if (res.Text.Contains("接触", StringComparison.Ordinal)
|| res.Text.Contains("地脉", StringComparison.Ordinal)
|| res.Text.Contains("之花", StringComparison.Ordinal))
{
return true;
}
}
return false;
}
private void LogRewardNav(string message, params object[] args)
{
var now = DateTime.UtcNow;
// Throttle log spam during navigation loops.
if ((now - _lastRewardNavLog).TotalSeconds < 3)
{
return;
}
_lastRewardNavLog = now;
if (args.Length == 0)
{
_logger.LogInformation(message);
}
else
{
_logger.LogInformation(message, args);
}
}
private async Task<bool> ProcessResurrect()
{
using var capture = CaptureToRectArea();
var list = capture.FindMulti(_ocrRoThis);
foreach (var res in list)
{
if (res.Text.Contains("复苏", StringComparison.Ordinal))
{
res.Click();
await Delay(2000, _ct);
return true;
}
}
return false;
}
private async Task SwitchToFriendshipTeamIfNeeded()
{
if (string.IsNullOrWhiteSpace(_taskParam.FriendshipTeam))
{
_friendshipTeamSwitched = false;
return;
}
Simulation.SendInput.SimulateAction(GIActions.MoveForward, KeyType.KeyUp);
try
{
_friendshipTeamSwitched = await TrySwitchPartyAndSync(_taskParam.FriendshipTeam);
if (!_friendshipTeamSwitched)
{
_logger.LogWarning("切换好感队失败,保持当前队伍");
}
}
catch (Exception ex)
{
_friendshipTeamSwitched = false;
_logger.LogWarning(ex, "切换好感队失败!");
}
}
private async Task SwitchBackToCombatTeam()
{
if (!_friendshipTeamSwitched || string.IsNullOrWhiteSpace(_taskParam.Team))
{
return;
}
Simulation.SendInput.SimulateAction(GIActions.MoveForward, KeyType.KeyUp);
await TrySwitchPartyAndSync(_taskParam.Team);
_friendshipTeamSwitched = false;
}
private async Task<bool> TrySwitchPartyAndSync(string partyName)
{
if (string.IsNullOrWhiteSpace(partyName))
{
return false;
}
_switchPartyTask ??= new SwitchPartyTask();
var success = await _switchPartyTask.Start(partyName, _ct);
if (success)
{
RunnerContext.Instance.PartyName = partyName;
}
return success;
}
private async Task<bool> AttemptReward(int retryCount = 0)
{
const int maxRetry = 3;
if (retryCount >= maxRetry)
{
throw new Exception("领取奖励失败");
}
Simulation.SendInput.SimulateAction(GIActions.PickUpOrInteract);
await Delay(800, _ct);
if (!await VerifyRewardPage())
{
await _returnMainUiTask.Start(_ct);
await AutoNavigateToReward();
return await AttemptReward(retryCount + 1);
}
if (!await TryUseRewardResin())
{
await EnsureExitRewardPage();
return false;
}
if (_friendshipTeamSwitched)
{
await SwitchBackToCombatTeam();
}
else if (!string.IsNullOrWhiteSpace(_taskParam.FriendshipTeam))
{
_logger.LogDebug("本次未成功切换到好感队,跳过切回战斗队");
}
await Delay(1200, _ct);
await EnsureExitRewardPage();
return true;
}
private async Task<bool> VerifyRewardPage()
{
using var capture = CaptureToRectArea();
return HasRewardPrompt(capture);
}
private IDisposable DrawOcrOverlayScope(ImageRegion capture, string key, params Rect[] rois)
{
var drawList = new List<RectDrawable>(rois.Length);
foreach (var roi in rois)
{
var clamped = roi.ClampTo(capture.Width, capture.Height);
if (clamped.Width <= 0 || clamped.Height <= 0)
{
continue;
}
drawList.Add(capture.ToRectDrawable(clamped, key, OcrOverlayPen));
}
var drawContent = VisionContext.Instance().DrawContent;
drawContent.PutOrRemoveRectList(key, drawList.Count > 0 ? drawList : null);
RefreshMaskWindowForOverlay();
return new OcrOverlayScope(drawContent, key, RefreshMaskWindowForOverlay);
}
private void ClearOcrOverlayKeys()
{
var drawContent = VisionContext.Instance().DrawContent;
drawContent.RemoveRect(OcrFlowOverlayKey);
drawContent.RemoveRect(OcrFightOverlayKey);
drawContent.PutOrRemoveTextList(OcrFlowOverlayKey, null);
drawContent.PutOrRemoveTextList(OcrFightOverlayKey, null);
RefreshMaskWindowForOverlay();
}
private async Task WaitOcrOverlayRenderTick()
{
await Task.Yield();
await Task.Delay(OcrOverlayRenderLeadMs, _ct);
}
private void EnsureMaskOverlayVisible()
{
var config = TaskContext.Instance().Config.MaskWindowConfig;
_overlayDisplayOriginalValue = config.DisplayRecognitionResultsOnMask;
if (!config.DisplayRecognitionResultsOnMask)
{
config.DisplayRecognitionResultsOnMask = true;
_overlayDisplayTemporarilyEnabled = true;
}
var maskWindow = MaskWindow.InstanceNullable();
if (maskWindow != null)
{
maskWindow.Invoke(() =>
{
maskWindow.Topmost = true;
if (!maskWindow.IsVisible)
{
maskWindow.Show();
}
maskWindow.BringToTop();
});
}
}
private void RestoreMaskOverlayVisible()
{
if (!_overlayDisplayTemporarilyEnabled)
{
return;
}
TaskContext.Instance().Config.MaskWindowConfig.DisplayRecognitionResultsOnMask = _overlayDisplayOriginalValue;
_overlayDisplayTemporarilyEnabled = false;
}
private void RefreshMaskWindowForOverlay()
{
var maskWindow = MaskWindow.InstanceNullable();
if (maskWindow == null)
{
return;
}
var now = DateTime.UtcNow;
var shouldBringTop = now - _lastMaskBringTopTime > TimeSpan.FromSeconds(1);
if (shouldBringTop)
{
_lastMaskBringTopTime = now;
}
maskWindow.Invoke(() =>
{
maskWindow.Topmost = true;
if (!maskWindow.IsVisible)
{
maskWindow.Show();
}
if (shouldBringTop)
{
maskWindow.BringToTop();
}
maskWindow.Refresh();
});
}
private async Task<bool> TryUseRewardResin()
{
for (var attempt = 0; attempt < 2; attempt++)
{
if (await TryUseRewardResinOnce())
{
await Delay(1000, _ct);
if (!await VerifyRewardPage())
{
return true;
}
_logger.LogDebug("树脂点击后奖励页面仍存在,第{Attempt}次后准备重试", attempt + 1);
}
else
{
_logger.LogDebug("奖励页面未成功选择树脂,第{Attempt}次后准备重试", attempt + 1);
}
await Delay(300, _ct);
}
return false;
}
private async Task<bool> TryUseRewardResinOnce()
{
await ActivateRewardPrompt();
var promptRegions = CaptureRewardPromptRegions();
if (promptRegions.Count == 0)
{
_logger.LogDebug("奖励页面未识别到树脂弹窗内容");
return false;
}
var lineTexts = BuildPromptTextLines(promptRegions);
var isOriginalResinEmpty = lineTexts.Any(text => text.Contains("补充", StringComparison.Ordinal));
var hasDoubleReward = lineTexts.Any(text => text.Contains("双倍", StringComparison.Ordinal)
|| text.Contains("2倍产出", StringComparison.Ordinal)
|| text.Contains("2倍", StringComparison.Ordinal));
var originalResinLines = lineTexts.Where(text => text.Contains("原粹", StringComparison.Ordinal)).ToList();
var hasOriginal20 = !isOriginalResinEmpty && originalResinLines.Any(text => text.Contains("20", StringComparison.Ordinal));
var hasOriginal40 = !isOriginalResinEmpty && originalResinLines.Any(text => text.Contains("40", StringComparison.Ordinal));
var hasCondensed = lineTexts.Any(text => text.Contains("浓缩", StringComparison.Ordinal));
var hasTransient = lineTexts.Any(text => text.Contains("须臾", StringComparison.Ordinal));
var hasFragile = lineTexts.Any(text => text.Contains("脆弱", StringComparison.Ordinal));
// 双倍奖励下优先切到 40 树脂,避免误用 20 树脂。
if (hasDoubleReward && hasOriginal20 && !hasOriginal40)
{
if (await TrySwitch20To40Resin())
{
await Delay(300, _ct);
promptRegions = CaptureRewardPromptRegions();
if (promptRegions.Count == 0)
{
return false;
}
lineTexts = BuildPromptTextLines(promptRegions);
isOriginalResinEmpty = lineTexts.Any(text => text.Contains("补充", StringComparison.Ordinal));
hasDoubleReward = lineTexts.Any(text => text.Contains("双倍", StringComparison.Ordinal)
|| text.Contains("2倍产出", StringComparison.Ordinal)
|| text.Contains("2倍", StringComparison.Ordinal));
originalResinLines = lineTexts.Where(text => text.Contains("原粹", StringComparison.Ordinal)).ToList();
hasOriginal20 = !isOriginalResinEmpty && originalResinLines.Any(text => text.Contains("20", StringComparison.Ordinal));
hasOriginal40 = !isOriginalResinEmpty && originalResinLines.Any(text => text.Contains("40", StringComparison.Ordinal));
hasCondensed = lineTexts.Any(text => text.Contains("浓缩", StringComparison.Ordinal));
hasTransient = lineTexts.Any(text => text.Contains("须臾", StringComparison.Ordinal));
hasFragile = lineTexts.Any(text => text.Contains("脆弱", StringComparison.Ordinal));
}
}
var candidates = new List<string>();
if (hasDoubleReward && (hasOriginal20 || hasOriginal40))
{
candidates.Add("原粹树脂");
}
else if (isOriginalResinEmpty)
{
if (hasCondensed)
{
candidates.Add("浓缩树脂");
}
if (hasTransient && _taskParam.UseTransientResin)
{
candidates.Add("须臾树脂");
}
if (hasFragile && _taskParam.UseFragileResin)
{
candidates.Add("脆弱树脂");
}
}
else
{
if (hasCondensed)
{
candidates.Add("浓缩树脂");
}
if (hasTransient && _taskParam.UseTransientResin)
{
candidates.Add("须臾树脂");
}
if (hasOriginal20 || hasOriginal40)
{
candidates.Add("原粹树脂");
}
if (hasFragile && _taskParam.UseFragileResin)
{
candidates.Add("脆弱树脂");
}
}
foreach (var resinName in candidates.Distinct(StringComparer.Ordinal))
{
if (await TryPressRewardResin(promptRegions, resinName))
{
return true;
}
}
_logger.LogDebug("奖励页面树脂识别结果未匹配成功ocr={Texts}", string.Join(" | ", lineTexts));
return false;
}
private async Task ActivateRewardPrompt()
{
using var capture = CaptureToRectArea();
var titleRoi = GetRewardPromptTitleRoi(capture);
var titleRegion = CaptureRewardPromptTitleRegion(capture, titleRoi);
// 对齐自动秘境的处理,先点一次标题区域激活弹窗,再点树脂使用按钮。
Simulation.SendInput.Mouse.LeftButtonUp();
await Delay(60, _ct);
if (titleRegion != null)
{
titleRegion.Click();
_logger.LogDebug("奖励页面已点击标题激活弹窗text={Text}, x={X}, y={Y}", titleRegion.Text, titleRegion.X, titleRegion.Y);
}
else
{
capture.Derive(titleRoi).Click();
_logger.LogDebug("奖励页面标题 OCR 被拆分,回退点击标题区域中心激活弹窗");
}
await Delay(800, _ct);
}
private Region? CaptureRewardPromptTitleRegion(ImageRegion capture, Rect titleRoi)
{
var titleRegions = capture.FindMulti(RecognitionObject.Ocr(titleRoi));
var mergedLines = MergeTextRegionsByLine(capture, titleRegions);
return mergedLines.FirstOrDefault(r => IsRewardPromptTitleText(r.Text));
}
private static bool IsRewardPromptTitleText(string text)
{
text = NormalizeLeyLineOcrText(text);
return text.Contains("激活地脉之花", StringComparison.Ordinal)
|| text.Contains("选择激活方式", StringComparison.Ordinal)
|| text.Contains("地脉之花", StringComparison.Ordinal);
}
private List<Region> CaptureRewardPromptRegions()
{
using var capture = CaptureToRectArea();
return capture.FindMulti(RecognitionObject.Ocr(GetRewardPromptContentRoi(capture)));
}
private async Task<bool> TryPressRewardResin(List<Region> promptRegions, string resinName)
{
// 某些链路会残留左键按下状态,先显式抬起一次再点使用按钮。
Simulation.SendInput.Mouse.LeftButtonUp();
await Delay(60, _ct);
var (success, _) = AutoDomainTask.PressUseResin(promptRegions, resinName, Name);
if (success)
{
_logger.LogDebug("奖励页面已尝试使用树脂:{ResinName}", resinName);
}
return success;
}
private async Task<bool> TrySwitch20To40Resin()
{
var switchRo = BuildTemplate("Assets/icon/switch_button.png", null, 0.7);
using var capture = CaptureToRectArea();
var res = capture.Find(switchRo);
if (res.IsEmpty())
{
return false;
}
res.Click();
await Delay(800, _ct);
using var check = CaptureToRectArea();
var lineTexts = BuildPromptTextLines(check.FindMulti(_ocrRoThis));
return lineTexts.Any(text => text.Contains("40", StringComparison.Ordinal) && text.Contains("原粹", StringComparison.Ordinal));
}
private static Rect GetRewardPromptTitleRoi(ImageRegion capture)
{
return new Rect(capture.Width / 4, capture.Height * 3 / 20, capture.Width / 2, capture.Height / 4);
}
private static Rect GetRewardPromptContentRoi(ImageRegion capture)
{
return new Rect(capture.Width / 4, capture.Height / 5, capture.Width / 2, capture.Height * 3 / 5);
}
private static List<string> BuildPromptTextLines(IEnumerable<Region> regions)
{
return GroupPromptRegionsByLine(regions)
.Select(line => NormalizeLeyLineOcrText(string.Concat(line.OrderBy(r => r.X).Select(r => r.Text.Trim()))))
.Where(text => !string.IsNullOrWhiteSpace(text))
.ToList();
}
private bool HasRewardPrompt(ImageRegion capture)
{
var titleRoi = GetRewardPromptTitleRoi(capture);
if (CaptureRewardPromptTitleRegion(capture, titleRoi) != null)
{
return true;
}
var promptRegions = capture.FindMulti(RecognitionObject.Ocr(GetRewardPromptContentRoi(capture)));
var lineTexts = BuildPromptTextLines(promptRegions);
return lineTexts.Any(ContainsRewardPromptContentText);
}
private static string NormalizeLeyLineOcrText(string? text)
{
if (string.IsNullOrWhiteSpace(text))
{
return string.Empty;
}
return text
.Replace("脈", "脉", StringComparison.Ordinal)
.Replace("觸", "触", StringComparison.Ordinal)
.Replace("樹", "树", StringComparison.Ordinal)
.Replace("選", "选", StringComparison.Ordinal)
.Replace("擇", "择", StringComparison.Ordinal)
.Replace("\r", string.Empty, StringComparison.Ordinal)
.Trim();
}
private static bool ContainsLeyLineFlowerText(string text)
{
text = NormalizeLeyLineOcrText(text);
return text.Contains("地脉之花", StringComparison.Ordinal)
|| (text.Contains("地脉", StringComparison.Ordinal) && text.Contains("之花", StringComparison.Ordinal));
}
private static bool ContainsRewardPromptActionText(string text)
{
text = NormalizeLeyLineOcrText(text);
return text.Contains("使用", StringComparison.Ordinal);
}
private static bool ContainsRewardPromptContentText(string text)
{
text = NormalizeLeyLineOcrText(text);
return text.Contains("原粹树脂", StringComparison.Ordinal)
|| text.Contains("浓缩树脂", StringComparison.Ordinal)
|| text.Contains("须臾树脂", StringComparison.Ordinal)
|| text.Contains("脆弱树脂", StringComparison.Ordinal)
|| text.Contains("激活地脉之花", StringComparison.Ordinal)
|| text.Contains("选择激活方式", StringComparison.Ordinal)
|| (text.Contains("树脂", StringComparison.Ordinal) && text.Contains("使用", StringComparison.Ordinal))
|| text.Contains("补充", StringComparison.Ordinal);
}
private static List<Region> MergeTextRegionsByLine(Region owner, IEnumerable<Region> regions)
{
var merged = new List<Region>();
foreach (var line in GroupPromptRegionsByLine(regions))
{
var orderedLine = line.OrderBy(r => r.X).ToList();
var left = orderedLine.Min(r => r.X);
var top = orderedLine.Min(r => r.Y);
var right = orderedLine.Max(r => r.Right);
var bottom = orderedLine.Max(r => r.Bottom);
var mergedRegion = owner.Derive(left, top, right - left, bottom - top);
mergedRegion.Text = string.Concat(orderedLine.Select(r => r.Text.Trim()));
merged.Add(mergedRegion);
}
return merged.OrderBy(r => r.Y).ThenBy(r => r.X).ToList();
}
private static List<List<Region>> GroupPromptRegionsByLine(IEnumerable<Region> regions)
{
var ordered = regions
.Where(r => !string.IsNullOrWhiteSpace(r.Text))
.OrderBy(r => r.Y)
.ThenBy(r => r.X)
.ToList();
var lines = new List<List<Region>>();
foreach (var region in ordered)
{
var line = lines.FirstOrDefault(candidate => IsSamePromptLine(candidate[0], region));
if (line == null)
{
lines.Add(new List<Region> { region });
}
else
{
line.Add(region);
}
}
return lines;
}
private static bool IsSamePromptLine(Region first, Region second)
{
var firstCenter = first.Y + first.Height / 2.0;
var secondCenter = second.Y + second.Height / 2.0;
var tolerance = Math.Max(first.Height, second.Height) * 0.6;
return Math.Abs(firstCenter - secondCenter) <= tolerance;
}
private async Task EnsureExitRewardPage()
{
const int maxAttempts = 5;
for (var i = 0; i < maxAttempts; i++)
{
if (!await VerifyRewardPage())
{
return;
}
Simulation.SendInput.Keyboard.KeyPress(User32.VK.VK_ESCAPE);
await Delay(800, _ct);
}
}
private async Task CloseCustomMarks()
{
await _returnMainUiTask.Start(_ct);
Simulation.SendInput.SimulateAction(GIActions.OpenMap);
await Delay(1000, _ct);
GameCaptureRegion.GameRegion1080PPosClick(60, 1020);
await Delay(600, _ct);
using var capture = CaptureToRectArea();
if (_openRo == null)
{
return;
}
var button = capture.Find(_openRo);
if (button.IsExist())
{
_marksStatus = false;
button.Click();
await Delay(600, _ct);
}
Simulation.SendInput.Keyboard.KeyPress(User32.VK.VK_ESCAPE);
}
private async Task OpenCustomMarks()
{
await _returnMainUiTask.Start(_ct);
Simulation.SendInput.SimulateAction(GIActions.OpenMap);
await Delay(1000, _ct);
GameCaptureRegion.GameRegion1080PPosClick(60, 1020);
await Delay(600, _ct);
if (_closeRo == null)
{
return;
}
using var capture = CaptureToRectArea();
var buttons = capture.FindMulti(_closeRo);
foreach (var button in buttons)
{
if (button.Y > ScaleTo1080(280) && button.Y < ScaleTo1080(350))
{
button.Click();
_marksStatus = true;
break;
}
}
}
private async Task FindLeyLineOutcropByBook(string country, string type)
{
await OpenLeyLineOutcropCountryInHandbook(country, type);
for (var retry = 0; retry < 3; retry++)
{
if (await TryOpenBigMapFromHandbook())
{
break;
}
if (retry < 2)
{
_logger.LogDebug("通过冒险之证打开大地图失败,重新打开冒险之证,第{Retry}次", retry + 1);
await OpenLeyLineOutcropCountryInHandbook(country, type);
}
else
{
throw new Exception("大地图打开失败");
}
}
var center = _tpTask.GetBigMapCenterPoint(MapTypes.Teyvat.ToString());
_leyLineX = center.X;
_leyLineY = center.Y;
await CancelTrackingInMap();
}
private async Task OpenLeyLineOutcropCountryInHandbook(string country, string type)
{
await _returnMainUiTask.Start(_ct);
await Delay(1000, _ct);
Simulation.SendInput.SimulateAction(GIActions.OpenAdventurerHandbook);
await Delay(2500, _ct);
GameCaptureRegion.GameRegion1080PPosClick(300, 550);
await Delay(1000, _ct);
GameCaptureRegion.GameRegion1080PPosClick(500, 200);
await Delay(1000, _ct);
GameCaptureRegion.GameRegion1080PPosClick(500, 500);
await Delay(1000, _ct);
if (type == "启示之花")
{
GameCaptureRegion.GameRegion1080PPosClick(700, 350);
}
else
{
GameCaptureRegion.GameRegion1080PPosClick(500, 350);
}
await Delay(1000, _ct);
GameCaptureRegion.GameRegion1080PPosClick(1300, 800);
await Delay(1000, _ct);
await FindAndClickCountry(country);
}
private async Task<bool> CheckBigMapOpened()
{
if (_mapSettingButtonRo == null)
{
return false;
}
using var capture = CaptureToRectArea();
return capture.Find(_mapSettingButtonRo).IsExist();
}
private async Task FindAndClickCountry(string country)
{
var match = country == "挪德卡莱" ? "挪德卡" : country;
using var capture = CaptureToRectArea();
var list = capture.FindMulti(_ocrRoThis);
var target = list.FirstOrDefault(r => r.Text.Contains(match, StringComparison.Ordinal));
if (target == null)
{
throw new Exception($"冒险之证未找到国家: {country}");
}
target.Click();
}
private async Task<bool> TryOpenBigMapFromHandbook()
{
for (var attempt = 0; attempt < 2; attempt++)
{
var button = FindHandbookTrackActionButtonByIcon();
if (button == null)
{
break;
}
button.Click();
_logger.LogDebug("通过图标识别点击冒险之证底部操作按钮,第{Attempt}次", attempt + 1);
await Delay(1500, _ct);
if (await CheckBigMapOpened())
{
return true;
}
}
GameCaptureRegion.GameRegion1080PPosClick(1500, 850);
_logger.LogDebug("图标未命中或点击后未打开大地图,回退固定坐标点击");
await Delay(2500, _ct);
return await CheckBigMapOpened();
}
private Region? FindHandbookTrackActionButtonByIcon()
{
if (_handbookTrackActionRo == null)
{
return null;
}
using var capture = CaptureToRectArea();
var button = capture.Find(_handbookTrackActionRo);
if (!button.IsExist())
{
return null;
}
return button;
}
private async Task CancelTrackingInMap()
{
GameCaptureRegion.GameRegion1080PPosClick(960, 540);
await Delay(1000, _ct);
using var capture = CaptureToRectArea();
var list = capture.FindMulti(_ocrRoThis);
var stop = list.FirstOrDefault(r => r.Text.Contains("停止", StringComparison.Ordinal));
if (stop != null)
{
stop.Click();
return;
}
var leyLine = list.FirstOrDefault(r => r.Text.Contains("地脉", StringComparison.Ordinal) || r.Text.Contains("衍出", StringComparison.Ordinal));
if (leyLine != null)
{
leyLine.Click();
await Delay(1000, _ct);
GameCaptureRegion.GameRegion1080PPosClick(1700, 1010);
await Delay(1000, _ct);
}
}
private async Task RecheckResinAndContinue()
{
_recheckCount++;
if (_taskParam.OpenModeCountMin)
{
if (_currentRunTimes >= _taskParam.Count)
{
return;
}
}
if (_recheckCount > MaxRecheckCount)
{
return;
}
var result = await CalCountByResin();
if (result.Count <= 0)
{
return;
}
if (result.Count > 50)
{
return;
}
_currentRunTimes = 0;
_taskParam.Count = result.Count;
await RunLeyLineChallenges();
await RecheckResinAndContinue();
}
private async Task<ResinCountResult> CalCountByResin()
{
var counts = await CountAllResin();
var originalTimes = counts.OriginalResinCount / 40;
var remaining = counts.OriginalResinCount % 40;
if (remaining >= 20)
{
originalTimes += remaining / 20;
}
var condensedTimes = counts.CondensedResinCount;
var transientTimes = _taskParam.UseTransientResin ? counts.TransientResinCount : 0;
var fragileTimes = _taskParam.UseFragileResin ? counts.FragileResinCount : 0;
return new ResinCountResult
{
Count = originalTimes + condensedTimes + transientTimes + fragileTimes,
OriginalResinTimes = originalTimes,
CondensedResinTimes = condensedTimes,
TransientResinTimes = transientTimes,
FragileResinTimes = fragileTimes
};
}
private async Task<ResinCounts> CountAllResin()
{
await _returnMainUiTask.Start(_ct);
await Delay(1500, _ct);
Simulation.SendInput.SimulateAction(GIActions.OpenMap);
await Delay(1500, _ct);
var result = new ResinCounts
{
OriginalResinCount = await CountOriginalResin(),
CondensedResinCount = await CountCondensedResin()
};
if (_taskParam.UseTransientResin || _taskParam.UseFragileResin)
{
await OpenReplenishResinUi();
await Delay(1500, _ct);
result.TransientResinCount = await CountTransientResin();
result.FragileResinCount = await CountFragileResin();
}
await _returnMainUiTask.Start(_ct);
return result;
}
private async Task<int> CountOriginalResin()
{
var icon = BuildTemplate("Assets/1920x1080/original_resin.png");
using var capture = CaptureToRectArea();
var res = capture.Find(icon);
if (res.IsEmpty())
{
return 0;
}
var roi = new Rect(res.X, res.Y, ScaleTo1080(200), ScaleTo1080(40));
using var region = capture.DeriveCrop(roi);
var text = OcrFactory.Paddle.OcrWithoutDetector(region.CacheGreyMat);
var match = Regex.Match(text, @"(\d{1,3})\s*/\s*\d+");
if (match.Success)
{
return int.TryParse(match.Groups[1].Value, out var value) ? value : 0;
}
return 0;
}
private async Task<int> CountCondensedResin()
{
var icon = BuildTemplate("Assets/1920x1080/condensed_resin.png");
using var capture = CaptureToRectArea();
var res = capture.Find(icon);
if (res.IsEmpty())
{
return 0;
}
var roi = new Rect(res.Right, res.Y, ScaleTo1080(90), ScaleTo1080(40));
using var region = capture.DeriveCrop(roi);
var text = OcrFactory.Paddle.OcrWithoutDetector(region.CacheGreyMat);
if (int.TryParse(Regex.Match(text, @"\d+").Value, out var value))
{
return value;
}
return await RecognizeWhiteNumber(region, capture);
}
private async Task<int> CountTransientResin()
{
var icon = BuildTemplate("Assets/1920x1080/transient_resin.png");
using var capture = CaptureToRectArea();
var res = capture.Find(icon);
if (res.IsEmpty())
{
return 0;
}
var roi = new Rect(res.X, res.Bottom, res.Width, ScaleTo1080(60));
using var region = capture.DeriveCrop(roi);
return await RecognizeNumberWithFallback(region);
}
private async Task<int> CountFragileResin()
{
var icon = BuildTemplate("Assets/1920x1080/fragile_resin.png");
using var capture = CaptureToRectArea();
var res = capture.Find(icon);
if (res.IsEmpty())
{
return 0;
}
var roi = new Rect(res.X, res.Bottom, res.Width, ScaleTo1080(60));
using var region = capture.DeriveCrop(roi);
return await RecognizeNumberWithFallback(region);
}
private async Task<int> RecognizeNumberWithFallback(ImageRegion region)
{
var text = OcrFactory.Paddle.OcrWithoutDetector(region.CacheGreyMat);
if (int.TryParse(Regex.Match(text, @"\d+").Value, out var value))
{
return value;
}
return await RecognizeNumberByTemplate(region, false);
}
private async Task<int> RecognizeWhiteNumber(ImageRegion region, ImageRegion capture)
{
return await RecognizeNumberByTemplate(region, true);
}
private async Task<int> RecognizeNumberByTemplate(ImageRegion region, bool white)
{
var icons = white
? new Dictionary<int, string>
{
{ 0, "Assets/1920x1080/num0_white.png" },
{ 1, "Assets/1920x1080/num1_white.png" },
{ 2, "Assets/1920x1080/num2_white.png" },
{ 3, "Assets/1920x1080/num3_white.png" },
{ 4, "Assets/1920x1080/num4_white.png" },
{ 5, "Assets/1920x1080/num5_white.png" }
}
: new Dictionary<int, string>
{
{ 1, "Assets/1920x1080/num1.png" },
{ 2, "Assets/1920x1080/num2.png" },
{ 3, "Assets/1920x1080/num3.png" },
{ 4, "Assets/1920x1080/num4.png" }
};
foreach (var kvp in icons)
{
var ro = BuildTemplate(kvp.Value);
var result = region.Find(ro);
if (result.IsExist())
{
return kvp.Key;
}
}
return 0;
}
private async Task OpenReplenishResinUi()
{
var ro = BuildTemplate("Assets/icon/replenish_resin_button.png");
using var capture = CaptureToRectArea();
var res = capture.Find(ro);
if (res.IsExist())
{
res.Click();
}
}
private class AutoLeyLineConfigData
{
[JsonPropertyName("errorThreshold")]
public double ErrorThreshold { get; set; }
[JsonPropertyName("mapPositions")]
public Dictionary<string, List<MapPosition>> MapPositions { get; set; } = [];
[JsonPropertyName("leyLinePositions")]
public Dictionary<string, List<LeyLinePosition>> LeyLinePositions { get; set; } = [];
}
private class MapPosition
{
[JsonPropertyName("x")]
public double X { get; set; }
[JsonPropertyName("y")]
public double Y { get; set; }
[JsonPropertyName("name")]
public string? Name { get; set; }
}
private class LeyLinePosition
{
[JsonPropertyName("x")]
public double X { get; set; }
[JsonPropertyName("y")]
public double Y { get; set; }
[JsonPropertyName("strategy")]
public string Strategy { get; set; } = string.Empty;
[JsonPropertyName("steps")]
public int Steps { get; set; }
[JsonPropertyName("order")]
public int Order { get; set; }
}
private class RawNodeData
{
[JsonPropertyName("teleports")]
public List<RawNode> Teleports { get; set; } = [];
[JsonPropertyName("blossoms")]
public List<RawNode> Blossoms { get; set; } = [];
[JsonPropertyName("edges")]
public List<RawEdge> Edges { get; set; } = [];
[JsonPropertyName("indexes")]
public Dictionary<string, Dictionary<string, List<int>>> Indexes { get; set; } = [];
}
private class RawNode
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("region")]
public string Region { get; set; } = string.Empty;
[JsonPropertyName("position")]
public NodePosition Position { get; set; } = new();
}
private class RawEdge
{
[JsonPropertyName("source")]
public int Source { get; set; }
[JsonPropertyName("target")]
public int Target { get; set; }
[JsonPropertyName("route")]
public string Route { get; set; } = string.Empty;
}
private class NodeData
{
public List<Node> Nodes { get; set; } = [];
public Dictionary<string, Dictionary<string, List<int>>> Indexes { get; set; } = [];
}
private class Node
{
public int Id { get; set; }
public string Region { get; set; } = string.Empty;
public NodePosition Position { get; set; } = new();
public string Type { get; set; } = string.Empty;
public List<NodeRoute> Next { get; set; } = [];
public List<int> Prev { get; set; } = [];
}
private class NodeRoute
{
public int Target { get; set; }
public string Route { get; set; } = string.Empty;
}
private class NodePosition
{
[JsonPropertyName("x")]
public double X { get; set; }
[JsonPropertyName("y")]
public double Y { get; set; }
}
private class PathInfo
{
public Node StartNode { get; set; } = new();
public Node TargetNode { get; set; } = new();
public List<string> Routes { get; set; } = [];
}
private class ResinCounts
{
public int OriginalResinCount { get; set; }
public int CondensedResinCount { get; set; }
public int TransientResinCount { get; set; }
public int FragileResinCount { get; set; }
}
private class ResinCountResult
{
public int Count { get; set; }
public int OriginalResinTimes { get; set; }
public int CondensedResinTimes { get; set; }
public int TransientResinTimes { get; set; }
public int FragileResinTimes { get; set; }
}
private sealed class OcrOverlayScope(DrawContent drawContent, string key, Action refreshAction) : IDisposable
{
private bool _disposed;
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
drawContent.RemoveRect(key);
drawContent.PutOrRemoveTextList(key, null);
refreshAction();
}
}
private sealed class AutoFightConfigScope(AllConfig allConfig, AutoFightConfig originalConfig) : IDisposable
{
private bool _disposed;
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
allConfig.AutoFightConfig = originalConfig;
}
}
}