Files
better-genshin-impact/BetterGenshinImpact/GameTask/AutoPathing/PathExecutor.cs
2025-10-10 02:51:38 +08:00

1347 lines
50 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.Config;
using BetterGenshinImpact.Core.Simulator;
using BetterGenshinImpact.GameTask.AutoFight.Assets;
using BetterGenshinImpact.GameTask.AutoFight.Model;
using BetterGenshinImpact.GameTask.AutoGeniusInvokation.Exception;
using BetterGenshinImpact.GameTask.AutoGeniusInvokation.Model;
using BetterGenshinImpact.GameTask.AutoPathing.Handler;
using BetterGenshinImpact.GameTask.AutoPathing.Model;
using BetterGenshinImpact.GameTask.AutoPathing.Model.Enum;
using BetterGenshinImpact.GameTask.AutoSkip;
using BetterGenshinImpact.GameTask.AutoSkip.Assets;
using BetterGenshinImpact.GameTask.AutoTrackPath;
using BetterGenshinImpact.GameTask.Common.BgiVision;
using BetterGenshinImpact.GameTask.Common.Job;
using BetterGenshinImpact.GameTask.Common.Map;
using BetterGenshinImpact.GameTask.Model.Area;
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.Extensions.Logging;
using OpenCvSharp;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BetterGenshinImpact.Core.Recognition.OCR;
using BetterGenshinImpact.GameTask.AutoPathing.Suspend;
using BetterGenshinImpact.GameTask.Common;
using Vanara.PInvoke;
using static BetterGenshinImpact.GameTask.Common.TaskControl;
using static BetterGenshinImpact.GameTask.SystemControl;
using ActionEnum = BetterGenshinImpact.GameTask.AutoPathing.Model.Enum.ActionEnum;
using BetterGenshinImpact.Core.Simulator.Extensions;
using BetterGenshinImpact.GameTask;
using BetterGenshinImpact.GameTask.AutoPathing;
using BetterGenshinImpact.GameTask.Common.Element.Assets;
using BetterGenshinImpact.GameTask.Common.Exceptions;
using BetterGenshinImpact.GameTask.Common.Map.Maps;
using BetterGenshinImpact.GameTask.AutoFight;
namespace BetterGenshinImpact.GameTask.AutoPathing;
public class PathExecutor
{
private readonly CameraRotateTask _rotateTask;
private readonly TrapEscaper _trapEscaper;
private readonly BlessingOfTheWelkinMoonTask _blessingOfTheWelkinMoonTask = new();
private AutoSkipTrigger? _autoSkipTrigger;
public int SuccessFight = 0;
//路径追踪完全走完所有路径结束的标识
public bool SuccessEnd = false;
private PathingPartyConfig? _partyConfig;
private CancellationToken ct;
private PathExecutorSuspend pathExecutorSuspend;
public PathExecutor(CancellationToken ct)
{
_trapEscaper = new(ct);
_rotateTask = new(ct);
this.ct = ct;
pathExecutorSuspend = new PathExecutorSuspend(this);
}
public PathingPartyConfig PartyConfig
{
get => _partyConfig ?? PathingPartyConfig.BuildDefault();
set => _partyConfig = value;
}
/// <summary>
/// 判断是否中止地图追踪的条件
/// </summary>
public Func<ImageRegion, bool>? EndAction { get; set; }
private CombatScenes? _combatScenes;
// private readonly Dictionary<string, string> _actionAvatarIndexMap = new();
private DateTime _elementalSkillLastUseTime = DateTime.MinValue;
private DateTime _useGadgetLastUseTime = DateTime.MinValue;
private const int RetryTimes = 2;
private int _inTrap = 0;
//记录当前相关点位数组
public (int, List<WaypointForTrack>) CurWaypoints { get; set; }
//记录当前点位
public (int, WaypointForTrack) CurWaypoint { get; set; }
//记录恢复点位数组
private (int, List<WaypointForTrack>) RecordWaypoints { get; set; }
//记录恢复点位
private (int, WaypointForTrack) RecordWaypoint { get; set; }
//跳过除走路径以外的操作
private bool _skipOtherOperations = false;
// 最近一次获取派遣奖励的时间
private DateTime _lastGetExpeditionRewardsTime = DateTime.MinValue;
//当到达恢复点位
public void TryCloseSkipOtherOperations()
{
// Logger.LogWarning("判断是否跳过地图追踪:" + (CurWaypoint.Item1 < RecordWaypoint.Item1));
if (RecordWaypoints == CurWaypoints && CurWaypoint.Item1 < RecordWaypoint.Item1)
{
return;
}
if (_skipOtherOperations)
{
Logger.LogWarning("已到达上次点位,地图追踪功能恢复");
}
_skipOtherOperations = false;
}
//记录点位,方便后面恢复
public void StartSkipOtherOperations()
{
Logger.LogWarning("记录恢复点位,地图追踪将到达上次点位之前将跳过走路之外的操作");
_skipOtherOperations = true;
RecordWaypoints = CurWaypoints;
RecordWaypoint = CurWaypoint;
}
public async Task Pathing(PathingTask task)
{
// SuspendableDictionary;
const string sdKey = "PathExecutor";
var sd = RunnerContext.Instance.SuspendableDictionary;
sd.Remove(sdKey);
RunnerContext.Instance.SuspendableDictionary.TryAdd(sdKey, pathExecutorSuspend);
if (!task.Positions.Any())
{
Logger.LogWarning("没有路径点,寻路结束");
return;
}
// 切换队伍
if (!await SwitchPartyBefore(task))
{
return;
}
// 校验路径是否可以执行
if (!await ValidateGameWithTask(task))
{
return;
}
InitializePathing(task);
// 转换、按传送点分割路径
var waypointsList = ConvertWaypointsForTrack(task.Positions, task);
await Delay(100, ct);
Navigation.WarmUp(task.Info.MapMatchMethod); // 提前加载地图特征点
foreach (var waypoints in waypointsList) // 按传送点分割的路径
{
CurWaypoints = (waypointsList.FindIndex(wps => wps == waypoints), waypoints);
for (var i = 0; i < RetryTimes; i++)
{
try
{
await ResolveAnomalies(); // 异常场景处理
// 如果首个点是非TP点位强制设置在这个点位附近优先做局部匹配
if (waypoints[0].Type != WaypointType.Teleport.Code)
{
Navigation.SetPrevPosition((float)waypoints[0].X, (float)waypoints[0].Y);
}
foreach (var waypoint in waypoints) // 一条路径
{
CurWaypoint = (waypoints.FindIndex(wps => wps == waypoint), waypoint);
TryCloseSkipOtherOperations();
await RecoverWhenLowHp(waypoint); // 低血量恢复
if (waypoint.Type == WaypointType.Teleport.Code)
{
await HandleTeleportWaypoint(waypoint);
}
else
{
await BeforeMoveToTarget(waypoint);
// Path不用走得很近Target需要接近但都需要先移动到对应位置
if (waypoint.Type == WaypointType.Orientation.Code)
{
// 方位点,只需要朝向
// 考虑到方位点大概率是作为执行action的最后一个点所以放在此处处理不和传送点一样单独处理
await FaceTo(waypoint);
}
else if (waypoint.Action != ActionEnum.UpDownGrabLeaf.Code)
{
await MoveTo(waypoint);
}
await BeforeMoveCloseToTarget(waypoint);
if (IsTargetPoint(waypoint))
{
await MoveCloseTo(waypoint);
}
//skipOtherOperations如果重试则跳过相关操作
if ((!string.IsNullOrEmpty(waypoint.Action) && !_skipOtherOperations) ||
waypoint.Action == ActionEnum.CombatScript.Code)
{
//战斗前的节点记录,用于游泳检测回到战斗节点
AutoFightTask.FightWaypoint = waypoint.Action == ActionEnum.Fight.Code ? waypoint : null;
// 执行 action
await AfterMoveToTarget(waypoint);
}
}
}
if (waypoints == waypointsList.Last())
{
SuccessEnd = true;
}
break;
}
catch (HandledException handledException)
{
SuccessEnd = true;
break;
}
catch (NormalEndException normalEndException)
{
Logger.LogInformation(normalEndException.Message);
if (!RunnerContext.Instance.isAutoFetchDispatch && RunnerContext.Instance.IsContinuousRunGroup)
{
throw;
}
else
{
break;
}
}
catch (TaskCanceledException e)
{
if (!RunnerContext.Instance.isAutoFetchDispatch && RunnerContext.Instance.IsContinuousRunGroup)
{
throw;
}
else
{
break;
}
}
catch (RetryException retryException)
{
StartSkipOtherOperations();
Logger.LogWarning(retryException.Message);
}
catch (RetryNoCountException retryException)
{
//特殊情况下,重试不消耗次数
i--;
StartSkipOtherOperations();
Logger.LogWarning(retryException.Message);
}
finally
{
// 不管咋样,松开所有按键
Simulation.SendInput.Keyboard.KeyUp(User32.VK.VK_W);
Simulation.SendInput.Mouse.RightButtonUp();
}
}
}
}
private bool IsTargetPoint(WaypointForTrack waypoint)
{
// 方位点不需要接近
if (waypoint.Type == WaypointType.Orientation.Code || waypoint.Action == ActionEnum.UpDownGrabLeaf.Code)
{
return false;
}
var action = ActionEnum.GetEnumByCode(waypoint.Action);
if (action is not null && action.UseWaypointTypeEnum != ActionUseWaypointTypeEnum.Custom)
{
// 强制点位类型的 action以 action 为准
return action.UseWaypointTypeEnum == ActionUseWaypointTypeEnum.Target;
}
// 其余情况和没有action的情况以点位类型为准
return waypoint.Type == WaypointType.Target.Code;
}
private async Task<bool> SwitchPartyBefore(PathingTask task)
{
var ra = CaptureToRectArea();
// 切换队伍前判断是否全队死亡 // 可能队伍切换失败导致的死亡
if (Bv.ClickIfInReviveModal(ra))
{
await Bv.WaitForMainUi(ct); // 等待主界面加载完成
Logger.LogInformation("复苏完成");
await Delay(4000, ct);
// 血量肯定不满,直接去七天神像回血
await TpStatueOfTheSeven();
}
var pRaList = ra.FindMulti(AutoFightAssets.Instance.PRa); // 判断是否联机
if (pRaList.Count > 0)
{
Logger.LogInformation("处于联机状态下,不切换队伍");
}
else
{
if (PartyConfig is { Enabled: false })
{
// 调度器未配置的情况下,根据地图追踪条件配置切换队伍
var partyName = FilterPartyNameByConditionConfig(task);
if (!await SwitchParty(partyName))
{
Logger.LogError("切换队伍失败,无法执行此路径!请检查地图追踪设置!");
return false;
}
}
else if (!string.IsNullOrEmpty(PartyConfig.PartyName))
{
if (!await SwitchParty(PartyConfig.PartyName))
{
Logger.LogError("切换队伍失败,无法执行此路径!请检查配置组中的地图追踪配置!");
return false;
}
}
}
return true;
}
private void InitializePathing(PathingTask task)
{
LogScreenResolution();
WeakReferenceMessenger.Default.Send(new PropertyChangedMessage<object>(this,
"UpdateCurrentPathing", new object(), task));
}
private void LogScreenResolution()
{
var gameScreenSize = SystemControl.GetGameScreenRect(TaskContext.Instance().GameHandle);
if (gameScreenSize.Width * 9 != gameScreenSize.Height * 16)
{
Logger.LogError("游戏窗口分辨率不是 16:9 !当前分辨率为 {Width}x{Height} , 非 16:9 分辨率的游戏无法正常使用地图追踪功能!",
gameScreenSize.Width, gameScreenSize.Height);
throw new Exception("游戏窗口分辨率不是 16:9 !无法使用地图追踪功能!");
}
if (gameScreenSize.Width < 1920 || gameScreenSize.Height < 1080)
{
Logger.LogError("游戏窗口分辨率小于 1920x1080 !当前分辨率为 {Width}x{Height} , 小于 1920x1080 的分辨率的游戏地图追踪的效果非常差!",
gameScreenSize.Width, gameScreenSize.Height);
throw new Exception("游戏窗口分辨率小于 1920x1080 !无法使用地图追踪功能!");
}
}
/// <summary>
/// 切换队伍
/// </summary>
/// <param name="partyName"></param>
/// <returns></returns>
private async Task<bool> SwitchParty(string? partyName)
{
bool success = true;
if (!string.IsNullOrEmpty(partyName))
{
if (RunnerContext.Instance.PartyName == partyName)
{
return success;
}
bool forceTp = PartyConfig.IsVisitStatueBeforeSwitchParty;
if (forceTp) // 强制传送模式
{
await new TpTask(ct).TpToStatueOfTheSeven(); // fix typos
success = await new SwitchPartyTask().Start(partyName, ct);
}
else // 优先原地切换模式
{
try
{
success = await new SwitchPartyTask().Start(partyName, ct);
}
catch (PartySetupFailedException)
{
await new TpTask(ct).TpToStatueOfTheSeven();
success = await new SwitchPartyTask().Start(partyName, ct);
}
}
if (success)
{
RunnerContext.Instance.PartyName = partyName;
RunnerContext.Instance.ClearCombatScenes();
}
}
return success;
}
private static string? FilterPartyNameByConditionConfig(PathingTask task)
{
var pathingConditionConfig = TaskContext.Instance().Config.PathingConditionConfig;
var materialName = task.GetMaterialName();
var specialActions = task.Positions
.Select(p => p.Action)
.Where(action => !string.IsNullOrEmpty(action))
.Distinct()
.ToList();
var partyName = pathingConditionConfig.FilterPartyName(materialName, specialActions);
return partyName;
}
/// <summary>
/// 校验
/// </summary>
/// <param name="task"></param>
/// <returns></returns>
private async Task<bool> ValidateGameWithTask(PathingTask task)
{
_combatScenes = await RunnerContext.Instance.GetCombatScenes(ct);
if (_combatScenes == null)
{
return false;
}
// 没有强制配置的情况下,使用地图追踪内的条件配置
// 必须放在这里,因为要通过队伍识别来得到最终结果
var pathingConditionConfig = TaskContext.Instance().Config.PathingConditionConfig;
if (PartyConfig is { Enabled: false })
{
PartyConfig = pathingConditionConfig.BuildPartyConfigByCondition(_combatScenes);
}
// 校验角色是否存在
if (task.HasAction(ActionEnum.NahidaCollect.Code))
{
var avatar = _combatScenes.SelectAvatar("纳西妲");
if (avatar == null)
{
Logger.LogError("此路径存在纳西妲收集动作,队伍中没有纳西妲角色,无法执行此路径!");
return false;
}
// _actionAvatarIndexMap.Add("nahida_collect", avatar.Index.ToString());
}
// 把所有需要切换的角色编号记录下来
Dictionary<string, ElementalType> map = new()
{
{ ActionEnum.HydroCollect.Code, ElementalType.Hydro },
{ ActionEnum.ElectroCollect.Code, ElementalType.Electro },
{ ActionEnum.AnemoCollect.Code, ElementalType.Anemo }
};
foreach (var (action, el) in map)
{
if (!ValidateElementalActionAvatarIndex(task, action, el, _combatScenes))
{
return false;
}
}
return true;
}
private bool ValidateElementalActionAvatarIndex(PathingTask task, string action, ElementalType el,
CombatScenes combatScenes)
{
if (task.HasAction(action))
{
foreach (var avatar in combatScenes.GetAvatars())
{
if (ElementalCollectAvatarConfigs.Get(avatar.Name, el) != null)
{
return true;
}
}
Logger.LogError("此路径存在 {El}元素采集 动作,队伍中没有对应元素角色:{Names},无法执行此路径!", el.ToChinese(),
string.Join(",", ElementalCollectAvatarConfigs.GetAvatarNameList(el)));
return false;
}
else
{
return true;
}
}
private List<List<WaypointForTrack>> ConvertWaypointsForTrack(List<Waypoint> positions, PathingTask task)
{
// 把 X Y 转换为 MatX MatY
var allList = positions.Select(waypoint =>
{
WaypointForTrack wft = new WaypointForTrack(waypoint, task.Info.MapName, task.Info.MapMatchMethod);
wft.Misidentification=waypoint.PointExtParams.Misidentification;
wft.MonsterTag = waypoint.PointExtParams.MonsterTag;
wft.EnableMonsterLootSplit = waypoint.PointExtParams.EnableMonsterLootSplit;
return wft;
}).ToList();
// 按照WaypointType.Teleport.Code切割数组
var result = new List<List<WaypointForTrack>>();
var tempList = new List<WaypointForTrack>();
foreach (var waypoint in allList)
{
if (waypoint.Type == WaypointType.Teleport.Code)
{
if (tempList.Count > 0)
{
result.Add(tempList);
tempList = new List<WaypointForTrack>();
}
}
tempList.Add(waypoint);
}
result.Add(tempList);
return result;
}
/// <summary>
/// 尝试队伍回血,如果单人回血,由于记录检查时是哪位残血,则当作行走位处理。
/// </summary>
private async Task<bool> TryPartyHealing()
{
if (_combatScenes is null) return false;
foreach (var avatar in _combatScenes.GetAvatars())
{
if (avatar.Name == "白术")
{
if (avatar.TrySwitch())
{
//1命白术能两次
Simulation.SendInput.SimulateAction(GIActions.ElementalSkill);
await Delay(800, ct);
Simulation.SendInput.SimulateAction(GIActions.ElementalSkill);
await Delay(800, ct);
await SwitchAvatar(PartyConfig.MainAvatarIndex);
await Delay(4000, ct);
return true;
}
break;
}
else if (avatar.Name == "希格雯")
{
if (avatar.TrySwitch())
{
Simulation.SendInput.SimulateAction(GIActions.ElementalSkill);
await Delay(11000, ct);
await SwitchAvatar(PartyConfig.MainAvatarIndex);
return true;
}
break;
}
else if (avatar.Name == "珊瑚宫心海")
{
if (avatar.TrySwitch())
{
Simulation.SendInput.SimulateAction(GIActions.ElementalSkill);
await Delay(500, ct);
//尝试Q全队回血
Simulation.SendInput.SimulateAction(GIActions.ElementalBurst);
//单人血只给行走位加血
await SwitchAvatar(PartyConfig.MainAvatarIndex);
await Delay(5000, ct);
return true;
}
}
}
return false;
}
private async Task RecoverWhenLowHp(WaypointForTrack waypoint)
{
if (PartyConfig.OnlyInTeleportRecover && waypoint.Type != WaypointType.Teleport.Code)
{
return;
}
using var region = CaptureToRectArea();
if (Bv.CurrentAvatarIsLowHp(region) && !(await TryPartyHealing() && Bv.CurrentAvatarIsLowHp(region)))
{
Logger.LogInformation("当前角色血量过低,去七天神像恢复");
await TpStatueOfTheSeven();
throw new RetryException("回血完成后重试路线");
}
else if (Bv.ClickIfInReviveModal(region))
{
await Bv.WaitForMainUi(ct); // 等待主界面加载完成
Logger.LogInformation("复苏完成");
await Delay(4000, ct);
// 血量肯定不满,直接去七天神像回血
await TpStatueOfTheSeven();
throw new RetryException("回血完成后重试路线");
}
}
private async Task TpStatueOfTheSeven()
{
// tp 到七天神像回血
var tpTask = new TpTask(ct);
await RunnerContext.Instance.StopAutoPickRunTask(async () => await tpTask.TpToStatueOfTheSeven(), 5);
Logger.LogInformation("血量恢复完成。【设置】-【七天神像设置】可以修改回血相关配置。");
}
/// <summary>
/// 尝试自动领取派遣奖励,
/// </summary>
/// <returns>是否可以领取派遣奖励</returns>
private async Task<bool> TryGetExpeditionRewardsDispatch(TpTask? tpTask = null)
{
if (tpTask == null)
{
tpTask = new TpTask(ct);
}
// 最小5分钟间隔
if ((DateTime.UtcNow - _lastGetExpeditionRewardsTime).TotalMinutes < 5)
{
return false;
}
//打开大地图操作
await tpTask.OpenBigMapUi();
bool changeBigMap = false;
string adventurersGuildCountry =
TaskContext.Instance().Config.OtherConfig.AutoFetchDispatchAdventurersGuildCountry;
if (!RunnerContext.Instance.isAutoFetchDispatch && adventurersGuildCountry != "无")
{
var ra1 = CaptureToRectArea();
var textRect = new Rect(60, 20, 160, 260);
var textMat = new Mat(ra1.SrcMat, textRect);
string text = OcrFactory.Paddle.Ocr(textMat);
if (text.Contains("探索派遣奖励"))
{
changeBigMap = true;
Logger.LogInformation("开始自动领取派遣任务!");
try
{
RunnerContext.Instance.isAutoFetchDispatch = true;
await RunnerContext.Instance.StopAutoPickRunTask(
async () => await new GoToAdventurersGuildTask().Start(adventurersGuildCountry, ct, null, true),
5);
Logger.LogInformation("自动领取派遣结束,回归原任务!");
}
catch (Exception e)
{
Logger.LogInformation("未知原因,发生异常,尝试继续执行任务!");
}
finally
{
RunnerContext.Instance.isAutoFetchDispatch = false;
_lastGetExpeditionRewardsTime = DateTime.UtcNow; // 无论成功与否都更新时间
}
}
}
return changeBigMap;
}
private async Task HandleTeleportWaypoint(WaypointForTrack waypoint)
{
var forceTp = waypoint.Action == ActionEnum.ForceTp.Code;
TpTask tpTask = new TpTask(ct);
await TryGetExpeditionRewardsDispatch(tpTask);
var (tpX, tpY) = await tpTask.Tp(waypoint.GameX, waypoint.GameY, waypoint.MapName, forceTp);
var (tprX, tprY) = MapManager.GetMap(waypoint.MapName, waypoint.MapMatchMethod)
.ConvertGenshinMapCoordinatesToImageCoordinates((float)tpX, (float)tpY);
Navigation.SetPrevPosition(tprX, tprY); // 通过上一个位置直接进行局部特征匹配
await Delay(500, ct); // 多等一会
}
public async Task FaceTo(WaypointForTrack waypoint)
{
var screen = CaptureToRectArea();
var position = await GetPosition(screen, waypoint);
var targetOrientation = Navigation.GetTargetOrientation(waypoint, position);
Logger.LogDebug("朝向点,位置({x2},{y2})", $"{waypoint.GameX:F1}", $"{waypoint.GameY:F1}");
await _rotateTask.WaitUntilRotatedTo(targetOrientation, 2);
await Delay(500, ct);
}
public DateTime moveToStartTime;
public async Task MoveTo(WaypointForTrack waypoint)
{
// 切人
await SwitchAvatar(PartyConfig.MainAvatarIndex);
var screen = CaptureToRectArea();
var (position, additionalTimeInMs) = await GetPositionAndTime(screen, waypoint);
var targetOrientation = Navigation.GetTargetOrientation(waypoint, position);
Logger.LogDebug("粗略接近途经点,位置({x2},{y2})", $"{waypoint.GameX:F1}", $"{waypoint.GameY:F1}");
await _rotateTask.WaitUntilRotatedTo(targetOrientation, 5);
moveToStartTime = DateTime.UtcNow;
var lastPositionRecord = DateTime.UtcNow;
var fastMode = false;
var prevPositions = new List<Point2f>();
var fastModeColdTime = DateTime.MinValue;
int num = 0, distanceTooFarRetryCount = 0, consecutiveRotationCountBeyondAngle = 0;
// 按下w一直走
Simulation.SendInput.SimulateAction(GIActions.MoveForward, KeyType.KeyDown);
while (!ct.IsCancellationRequested)
{
if (!Simulation.IsKeyDown(GIActions.MoveForward.ToActionKey().ToVK()))
{
Simulation.SendInput.SimulateAction(GIActions.MoveForward, KeyType.KeyDown);
}
num++;
if ((DateTime.UtcNow - moveToStartTime).TotalSeconds > 240)
{
Logger.LogWarning("执行超时,放弃此次追踪");
throw new RetryException("路径点执行超时,放弃整条路径");
}
screen = CaptureToRectArea();
EndJudgment(screen);
// position = await GetPosition(screen, waypoint);
(position, additionalTimeInMs) = await GetPositionAndTime(screen, waypoint);
if (additionalTimeInMs>0)
{
if (!Simulation.IsKeyDown(GIActions.MoveForward.ToActionKey().ToVK()))
{
Simulation.SendInput.SimulateAction(GIActions.MoveForward, KeyType.KeyDown);
}
additionalTimeInMs = additionalTimeInMs + 1000;//当做起步补偿
}
var distance = Navigation.GetDistance(waypoint, position);
Debug.WriteLine($"接近目标点中,距离为{distance}");
if (distance < 4)
{
Logger.LogDebug("到达路径点附近");
break;
}
if (distance > 500)
{
if (pathExecutorSuspend.CheckAndResetSuspendPoint())
{
throw new RetryNoCountException("可能暂停导致路径过远,重试一次此路线!");
}
else
{
distanceTooFarRetryCount++;
if (distanceTooFarRetryCount > 50)
{
if (position == new Point2f())
{
throw new HandledException("重试多次后,当前点位无法被识别,放弃此路径!");
}
else
{
Logger.LogWarning($"距离过远({position.X},{position.Y}->{waypoint.X},{waypoint.Y}={distance},重试多次后仍然失败,放弃此路径点!");
throw new HandledException("目标距离过远,可能是当前点位无法识别,放弃此路径!");
}
}
else
{
// 取余减少日志输出频率
if (distanceTooFarRetryCount % 5 == 0)
{
Logger.LogWarning($"距离过远({position.X},{position.Y}->{waypoint.X},{waypoint.Y}={distance},重试");
}
await Delay(50, ct);
continue;
}
}
}
// 非攀爬状态下,检测是否卡死(脱困触发器)
if (waypoint.MoveMode != MoveModeEnum.Climb.Code)
{
if ((DateTime.UtcNow - lastPositionRecord).TotalMilliseconds > 1000 + additionalTimeInMs)
{
lastPositionRecord = DateTime.UtcNow;
prevPositions.Add(position);
if (prevPositions.Count > 8)
{
var delta = prevPositions[^1] - prevPositions[^8];
if (Math.Abs(delta.X) + Math.Abs(delta.Y) < 3)
{
_inTrap++;
if (_inTrap > 2)
{
throw new RetryException("此路线出现3次卡死重试一次路线或放弃此路线");
}
Logger.LogWarning("疑似卡死,尝试脱离...");
//调用脱困代码由TrapEscaper接管移动
await _trapEscaper.RotateAndMove();
await _trapEscaper.MoveTo(waypoint);
Simulation.SendInput.SimulateAction(GIActions.MoveForward, KeyType.KeyDown);
Logger.LogInformation("卡死脱离结束");
continue;
}
}
}
}
// 旋转视角
targetOrientation = Navigation.GetTargetOrientation(waypoint, position);
//执行旋转
var diff = _rotateTask.RotateToApproach(targetOrientation, screen);
if (num > 20)
{
if (Math.Abs(diff) > 5)
{
consecutiveRotationCountBeyondAngle++;
}
else
{
consecutiveRotationCountBeyondAngle = 0;
}
if (consecutiveRotationCountBeyondAngle > 10)
{
// 直接站定好转向
await _rotateTask.WaitUntilRotatedTo(targetOrientation, 2);
}
}
// 根据指定方式进行移动
if (waypoint.MoveMode == MoveModeEnum.Fly.Code)
{
var isFlying = Bv.GetMotionStatus(screen) == MotionStatus.Fly;
if (!isFlying)
{
Debug.WriteLine("未进入飞行状态,按下空格");
Simulation.SendInput.SimulateAction(GIActions.Jump);
await Delay(200, ct);
}
await Delay(100, ct);
continue;
}
if (waypoint.MoveMode == MoveModeEnum.Jump.Code)
{
Simulation.SendInput.SimulateAction(GIActions.Jump);
await Delay(200, ct);
continue;
}
// 只有设置为run才会一直疾跑
if (waypoint.MoveMode == MoveModeEnum.Run.Code)
{
if (distance > 20 != fastMode) // 距离大于20时可以使用疾跑/自由泳
{
if (fastMode)
{
Simulation.SendInput.SimulateAction(GIActions.SprintMouse, KeyType.KeyUp);
}
else
{
Simulation.SendInput.SimulateAction(GIActions.SprintMouse, KeyType.KeyDown);
}
fastMode = !fastMode;
}
}
else if (waypoint.MoveMode == MoveModeEnum.Dash.Code)
{
if (distance > 20) // 距离大于25时可以使用疾跑
{
if (Math.Abs((fastModeColdTime - DateTime.UtcNow).TotalMilliseconds) > 1000) //冷却一会
{
fastModeColdTime = DateTime.UtcNow;
Simulation.SendInput.SimulateAction(GIActions.SprintMouse);
}
}
}
else if (waypoint.MoveMode != MoveModeEnum.Climb.Code) //否则自动短疾跑
{
// 使用 E 技能
if (distance > 10 && !string.IsNullOrEmpty(PartyConfig.GuardianAvatarIndex) &&
double.TryParse(PartyConfig.GuardianElementalSkillSecondInterval, out var s))
{
if (s < 1)
{
Logger.LogWarning("元素战技冷却时间设置太短,不执行!");
return;
}
var ms = s * 1000;
if ((DateTime.UtcNow - _elementalSkillLastUseTime).TotalMilliseconds > ms)
{
// 可能刚切过人在冷却时间内
if (num <= 5 && (!string.IsNullOrEmpty(PartyConfig.MainAvatarIndex) &&
PartyConfig.GuardianAvatarIndex != PartyConfig.MainAvatarIndex))
{
await Delay(800, ct); // 总共1s
}
await UseElementalSkill();
_elementalSkillLastUseTime = DateTime.UtcNow;
}
}
// 自动疾跑
if (distance > 20 && PartyConfig.AutoRunEnabled)
{
if (Math.Abs((fastModeColdTime - DateTime.UtcNow).TotalMilliseconds) > 2500) //冷却时间2.5s,回复体力用
{
fastModeColdTime = DateTime.UtcNow;
Simulation.SendInput.SimulateAction(GIActions.SprintMouse);
}
}
}
// 使用小道具
if (PartyConfig.UseGadgetIntervalMs > 0)
{
if ((DateTime.UtcNow - _useGadgetLastUseTime).TotalMilliseconds > PartyConfig.UseGadgetIntervalMs)
{
Simulation.SendInput.SimulateAction(GIActions.QuickUseGadget);
_useGadgetLastUseTime = DateTime.UtcNow;
}
}
await Delay(100, ct);
}
// 抬起w键
Simulation.SendInput.SimulateAction(GIActions.MoveForward, KeyType.KeyUp);
}
private async Task UseElementalSkill()
{
if (string.IsNullOrEmpty(PartyConfig.GuardianAvatarIndex))
{
return;
}
await Delay(200, ct);
// 切人
Logger.LogInformation("切换盾、回血角色,使用元素战技");
var avatar = await SwitchAvatar(PartyConfig.GuardianAvatarIndex, true);
if (avatar == null)
{
return;
}
// 钟离往身后放柱子
if (avatar.Name == "钟离")
{
Simulation.SendInput.SimulateAction(GIActions.MoveForward, KeyType.KeyUp);
await Delay(50, ct);
Simulation.SendInput.SimulateAction(GIActions.MoveBackward);
await Delay(200, ct);
}
avatar.UseSkill(PartyConfig.GuardianElementalSkillLongPress);
// 钟离往身后放柱子 后继续走路
if (avatar.Name == "钟离")
{
Simulation.SendInput.SimulateAction(GIActions.MoveForward, KeyType.KeyDown);
}
}
private async Task MoveCloseTo(WaypointForTrack waypoint)
{
ImageRegion screen;
Point2f position;
int targetOrientation;
Logger.LogDebug("精确接近目标点,位置({x2},{y2})", $"{waypoint.GameX:F1}", $"{waypoint.GameY:F1}");
var stepsTaken = 0;
while (!ct.IsCancellationRequested)
{
stepsTaken++;
if (stepsTaken > 25)
{
Logger.LogWarning("精确接近超时");
break;
}
screen = CaptureToRectArea();
EndJudgment(screen);
position = await GetPosition(screen, waypoint);
if (Navigation.GetDistance(waypoint, position) < 2)
{
Logger.LogDebug("已到达路径点");
break;
}
targetOrientation = Navigation.GetTargetOrientation(waypoint, position);
await _rotateTask.WaitUntilRotatedTo(targetOrientation, 2);
// 小碎步接近
Simulation.SendInput.SimulateAction(GIActions.MoveForward, KeyType.KeyDown);
Thread.Sleep(60);
Simulation.SendInput.SimulateAction(GIActions.MoveForward, KeyType.KeyUp);
// Simulation.SendInput.Keyboard.KeyDown(User32.VK.VK_W).Sleep(60).KeyUp(User32.VK.VK_W);
await Delay(20, ct);
}
Simulation.SendInput.SimulateAction(GIActions.MoveForward, KeyType.KeyUp);
// 到达目的地后停顿一秒
await Delay(1000, ct);
}
private async Task BeforeMoveCloseToTarget(WaypointForTrack waypoint)
{
if (waypoint.MoveMode == MoveModeEnum.Fly.Code && waypoint.Action == ActionEnum.StopFlying.Code)
{
await ActionFactory.GetBeforeHandler(ActionEnum.StopFlying.Code).RunAsync(ct, waypoint);
}
}
private async Task BeforeMoveToTarget(WaypointForTrack waypoint)
{
if (waypoint.Action == ActionEnum.UpDownGrabLeaf.Code)
{
Simulation.SendInput.Mouse.MiddleButtonClick();
await Delay(300, ct);
var screen = CaptureToRectArea();
var position = await GetPosition(screen, waypoint);
var targetOrientation = Navigation.GetTargetOrientation(waypoint, position);
await _rotateTask.WaitUntilRotatedTo(targetOrientation, 10);
var handler = ActionFactory.GetBeforeHandler(waypoint.Action);
await handler.RunAsync(ct, waypoint);
}
else if (waypoint.Action == ActionEnum.LogOutput.Code)
{
Logger.LogInformation(waypoint.LogInfo);
}
}
private async Task AfterMoveToTarget(WaypointForTrack waypoint)
{
if (waypoint.Action == ActionEnum.NahidaCollect.Code
|| waypoint.Action == ActionEnum.PickAround.Code
|| waypoint.Action == ActionEnum.Fight.Code
|| waypoint.Action == ActionEnum.HydroCollect.Code
|| waypoint.Action == ActionEnum.ElectroCollect.Code
|| waypoint.Action == ActionEnum.AnemoCollect.Code
|| waypoint.Action == ActionEnum.PyroCollect.Code
|| waypoint.Action == ActionEnum.CombatScript.Code
|| waypoint.Action == ActionEnum.Mining.Code
|| waypoint.Action == ActionEnum.Fishing.Code
|| waypoint.Action == ActionEnum.ExitAndRelogin.Code
|| waypoint.Action == ActionEnum.SetTime.Code
|| waypoint.Action == ActionEnum.UseGadget.Code)
{
var handler = ActionFactory.GetAfterHandler(waypoint.Action);
//,PartyConfig
await handler.RunAsync(ct, waypoint, PartyConfig);
//统计结束战斗的次数
if (waypoint.Action == ActionEnum.Fight.Code)
{
SuccessFight++;
}
await Delay(1000, ct);
}
}
private async Task<Avatar?> SwitchAvatar(string index, bool needSkill = false)
{
if (string.IsNullOrEmpty(index))
{
return null;
}
var avatar = _combatScenes?.SelectAvatar(int.Parse(index));
if (avatar == null) return null;
if (needSkill && !avatar.IsSkillReady())
{
Logger.LogInformation("角色{Name}技能未冷却,跳过。", avatar.Name);
return null;
}
var success = avatar.TrySwitch(5);//多切换一次,否则如果切人纠正要等下一个循环
if (success)
{
await Delay(100, ct);
return avatar;
}
Logger.LogInformation("尝试切换角色{Name}失败!", avatar.Name);
return null;
}
/// <summary>
/// 根据时间在两个点之间插值。
/// </summary>
/// <param name="startPoint">起点坐标</param>
/// <param name="endPoint">终点坐标</param>
/// <param name="startTime">起始时间</param>
/// <param name="midTime">中间时间</param>
/// <param name="endTime">结束时间</param>
/// <returns>中间点坐标</returns>
public static Point2f InterpolatePointByTime(
Point2f startPoint,
Point2f endPoint,
DateTime startTime,
DateTime midTime,
DateTime endTime)
{
// 计算时间差
double totalMillis = (endTime - startTime).TotalMilliseconds;
double midMillis = (midTime - startTime).TotalMilliseconds;
// 防止除以0
if (totalMillis == 0)
return startPoint;
// 计算比例
float t = (float)(midMillis / totalMillis);
if (t>1.0f)
{
t = 1.0f;
}
// 插值计算
float x = startPoint.X + (endPoint.X - startPoint.X) * t;
float y = startPoint.Y + (endPoint.Y - startPoint.Y) * t;
return new Point2f(x, y);
}
private Point2f prePosition;
private DateTime preTime;
//自动构造点位的最大时间
private int maxAutoPositionTime=10000;
private async Task WaitForCloseMap(int maxAttempts, int delayMs)
{
await Delay(delayMs, ct);
for (var i = 0; i < maxAttempts; i++)
{
using var capture = CaptureToRectArea();
if (Bv.IsInMainUi(capture))
{
return;
}
await Delay(delayMs, ct);
}
}
private async Task<Point2f> GetPosition(ImageRegion imageRegion, WaypointForTrack waypoint)
{
return (await GetPositionAndTime(imageRegion, waypoint)).point;
}
//
public bool GetPositionAndTimeSuspendFlag = false;
private async Task<(Point2f point,int additionalTimeInMs)> GetPositionAndTime(ImageRegion imageRegion, WaypointForTrack waypoint)
{
var position = Navigation.GetPosition(imageRegion, waypoint.MapName, waypoint.MapMatchMethod);
int time = 0;
if (position == new Point2f())
{
if (!Bv.IsInMainUi(imageRegion))
{
Logger.LogDebug("小地图位置定位失败,且当前不是主界面,进入异常处理");
await ResolveAnomalies(imageRegion);
}
}
var distance = Navigation.GetDistance(waypoint, position);
//中途暂停过,地图未识别到
if (position is {X:0,Y:0} && GetPositionAndTimeSuspendFlag)
{
GetPositionAndTimeSuspendFlag = false;
throw new RetryNoCountException("可能暂停导致路径过远,重试一次此路线!");
}
//何时处理 pathTooFar 路径过远 unrecognized 未识别
if ((position is {X:0,Y:0} && waypoint.Misidentification.Type.Contains("unrecognized")) || (distance>500 && waypoint.Misidentification.Type.Contains("pathTooFar")))
{
if (waypoint.Misidentification.HandlingMode == "previousDetectedPoint")
{
if (prePosition != default)
{
position = prePosition;
Logger.LogInformation(@$"未识别到具体路径,取上次点位");
}
}else if (waypoint.Misidentification.HandlingMode == "mapRecognition"){
//大地图识别坐标
DateTime start = DateTime.Now;
TpTask tpTask = new TpTask(ct);
await tpTask.OpenBigMapUi();
try
{
position =MapManager.GetMap(waypoint.MapName, waypoint.MapMatchMethod).ConvertGenshinMapCoordinatesToImageCoordinates(tpTask.GetPositionFromBigMap(waypoint.MapName));
}
catch (Exception e)
{
Logger.LogInformation(@$"地图中心点识别失败!");
}
Simulation.SendInput.Keyboard.KeyPress(User32.VK.VK_ESCAPE);
//Bv.IsInMainUi(imageRegion);
await WaitForCloseMap(10,200);
DateTime end = DateTime.Now;
time=(int)(end - start).TotalMilliseconds;
Logger.LogInformation(@$"未识别到具体路径,打开地图计算中心点({position.X},{position.Y})");
}
/*if (prePosition!=default)
{*/
//position = InterpolatePointByTime(prePosition,new Point2f((float)waypoint.GameX,(float)waypoint.GameY),preTime,DateTime.Now,preTime.AddMilliseconds(maxAutoPositionTime));
//Logger.LogInformation(@$"未识别到具体路径,预测其路径为({position.X},{position.Y},开始结束点位为:({prePosition.X},{prePosition.Y}{waypoint.GameX},{waypoint.GameY}");
//Point2f GetBigMapCenterPoint(string mapName)
// Logger.LogInformation(@$"未识别到具体路径,打开地图计算中心点({position.X},{position.Y})");
//position =prePosition;
// }
}
else
{
prePosition = position;
preTime = DateTime.Now;
}
//Logger.LogDebug("识别到路径:"+position.X+","+position.Y);
return (position,time);
}
/**
* 处理各种异常场景
* 需要保证耗时不能太高
*/
private async Task ResolveAnomalies(ImageRegion? imageRegion = null)
{
if (imageRegion == null)
{
imageRegion = CaptureToRectArea();
}
// 一些异常界面处理
var cookRa = imageRegion.Find(AutoSkipAssets.Instance.CookRo);
var closeRa = imageRegion.Find(AutoSkipAssets.Instance.PageCloseMainRo);
var closeRa2 = imageRegion.Find(ElementAssets.Instance.PageCloseWhiteRo);
if (cookRa.IsExist() || closeRa.IsExist() || closeRa2.IsExist())
{
// 排除大地图
if (Bv.IsInBigMapUi(imageRegion))
{
return;
}
Logger.LogInformation("检测到其他界面使用ESC关闭界面");
Simulation.SendInput.Keyboard.KeyPress(User32.VK.VK_ESCAPE);
await Delay(1000, ct); // 等待界面关闭
}
// 处理月卡
await _blessingOfTheWelkinMoonTask.Start(ct);
if (PartyConfig.AutoSkipEnabled)
{
// 判断是否进入剧情
await AutoSkip();
}
}
private async Task AutoSkip()
{
var ra = CaptureToRectArea();
var disabledUiButtonRa = ra.Find(AutoSkipAssets.Instance.DisabledUiButtonRo);
if (disabledUiButtonRa.IsExist())
{
Logger.LogWarning("进入剧情,自动点击剧情直到结束");
if (_autoSkipTrigger == null)
{
_autoSkipTrigger = new AutoSkipTrigger(new AutoSkipConfig
{
Enabled = true,
QuicklySkipConversationsEnabled = true, // 快速点击过剧情
ClosePopupPagedEnabled = true,
ClickChatOption = "优先选择最后一个选项",
});
_autoSkipTrigger.Init();
}
int noDisabledUiButtonTimes = 0;
while (true)
{
ra = CaptureToRectArea();
disabledUiButtonRa = ra.Find(AutoSkipAssets.Instance.DisabledUiButtonRo);
if (disabledUiButtonRa.IsExist())
{
_autoSkipTrigger.OnCapture(new CaptureContent(ra));
}
else
{
noDisabledUiButtonTimes++;
if (noDisabledUiButtonTimes > 10)
{
Logger.LogInformation("自动剧情结束");
break;
}
}
await Delay(210, ct);
}
}
}
private void EndJudgment(ImageRegion ra)
{
if (EndAction != null && EndAction(ra))
{
throw new HandledException("达成结束条件,结束地图追踪");
}
}
}