Files
火山 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

314 lines
10 KiB
C#
Raw Permalink 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 System;
using System.Drawing;
using System.Threading;
using System.Threading.Tasks;
using BetterGenshinImpact.Core.Simulator;
using BetterGenshinImpact.GameTask.AutoGeniusInvokation.Exception;
using BetterGenshinImpact.GameTask.Model.Area;
using Fischless.GameCapture;
using Microsoft.Extensions.Logging;
using OpenCvSharp;
using Vanara.PInvoke;
using BetterGenshinImpact.Core.Simulator.Extensions;
namespace BetterGenshinImpact.GameTask.Common;
public class TaskControl
{
public static ILogger Logger { get; } = App.GetLogger<TaskControl>();
public static readonly SemaphoreSlim TaskSemaphore = new(1, 1);
public static void CheckAndSleep(int millisecondsTimeout)
{
TrySuspend();
CheckAndActivateGameWindow();
Thread.Sleep(millisecondsTimeout);
}
public static void Sleep(int millisecondsTimeout)
{
NewRetry.Do(() =>
{
TrySuspend();
CheckAndActivateGameWindow();
}, TimeSpan.FromSeconds(1), 100);
Thread.Sleep(millisecondsTimeout);
}
private static bool IsKeyPressed(User32.VK key)
{
// 获取按键状态
var state = User32.GetAsyncKeyState((int)key);
// 检查高位是否为 1表示按键被按下
return (state & 0x8000) != 0;
}
public static void TrySuspend()
{
var first = true;
//此处为了记录最开始的暂停状态
var isSuspend = RunnerContext.Instance.IsSuspend;
while (RunnerContext.Instance.IsSuspend)
{
if (first)
{
RunnerContext.Instance.StopAutoPick();
//使快捷键本身释放
Thread.Sleep(300);
foreach (User32.VK key in Enum.GetValues(typeof(User32.VK)))
{
// 检查键是否被按下
if (IsKeyPressed(key)) // 强制转换 VK 枚举为 int
{
Logger.LogWarning($"解除{key}的按下状态.");
Simulation.SendInput.Keyboard.KeyUp(key);
}
}
Logger.LogWarning("快捷键触发暂停,等待解除");
foreach (var item in RunnerContext.Instance.SuspendableDictionary)
{
item.Value.Suspend();
}
first = false;
}
Thread.Sleep(1000);
}
//从暂停中解除
if (isSuspend)
{
Logger.LogWarning("暂停已经解除");
RunnerContext.Instance.ResumeAutoPick();
foreach (var item in RunnerContext.Instance.SuspendableDictionary)
{
item.Value.Resume();
}
}
}
private static void CheckAndActivateGameWindow()
{
if (!TaskContext.Instance().Config.OtherConfig.RestoreFocusOnLostEnabled)
{
if (!SystemControl.IsGenshinImpactActiveByProcess())
{
var name = SystemControl.GetActiveByProcess();
Logger.LogWarning($"当前获取焦点的窗口为: {name},不是原神,暂停");
throw new RetryException("当前获取焦点的窗口不是原神");
}
}
var count = 0;
//未激活则尝试恢复窗口
while (!SystemControl.IsGenshinImpactActiveByProcess())
{
if (count >= 10 && count % 10 == 0)
{
Logger.LogInformation("多次尝试未恢复,尝试最小化后激活窗口!");
SystemControl.MinimizeAndActivateWindow(TaskContext.Instance().GameHandle);
}
else
{
var name = SystemControl.GetActiveByProcess();
Logger.LogInformation("当前获取焦点的窗口为: {Name},不是原神,尝试恢复窗口", name);
SystemControl.FocusWindow(TaskContext.Instance().GameHandle);
}
count++;
Thread.Sleep(1000);
}
}
public static void Sleep(int millisecondsTimeout, CancellationToken ct)
{
if (ct.IsCancellationRequested)
{
throw new NormalEndException("取消自动任务");
}
if (millisecondsTimeout <= 0)
{
return;
}
NewRetry.Do(() =>
{
if (ct.IsCancellationRequested)
{
throw new NormalEndException("取消自动任务");
}
TrySuspend();
CheckAndActivateGameWindow();
}, TimeSpan.FromSeconds(1), 100);
Thread.Sleep(millisecondsTimeout);
if (ct.IsCancellationRequested)
{
throw new NormalEndException("取消自动任务");
}
}
public static async Task Delay(int millisecondsTimeout, CancellationToken ct)
{
if (ct is { IsCancellationRequested: true })
{
throw new NormalEndException("取消自动任务");
}
if (millisecondsTimeout <= 0)
{
return;
}
NewRetry.Do(() =>
{
if (ct is { IsCancellationRequested: true })
{
throw new NormalEndException("取消自动任务");
}
TrySuspend();
CheckAndActivateGameWindow();
}, TimeSpan.FromSeconds(1), 100);
await Task.Delay(millisecondsTimeout, ct);
if (ct is { IsCancellationRequested: true })
{
throw new NormalEndException("取消自动任务");
}
}
/// <summary>
/// 模拟长按指定动作。使用 try/finally 块确保在任务被取消或发生异常时,按键也能安全释放,防止卡键。
/// </summary>
/// <param name="action">需要模拟的游戏动作(如元素战技、普通攻击等)</param>
/// <param name="holdMs">长按持续的时间(毫秒)</param>
/// <param name="ct">用于监控任务取消的取消令牌</param>
public static async Task SimulateHoldActionAsync(GIActions action, int holdMs, CancellationToken ct)
{
try
{
Simulation.SendInput.SimulateAction(action, KeyType.KeyDown);
await Delay(holdMs, ct);
}
finally
{
Simulation.SendInput.SimulateAction(action, KeyType.KeyUp);
}
}
/// <summary>
/// 模拟长按元素战技如万叶长E。包含释放前摇、长按以及释放后的缓冲延时。
/// </summary>
/// <param name="holdMs">元素战技按住的时间(毫秒)</param>
/// <param name="ct">用于监控任务取消的取消令牌</param>
/// <param name="releaseLeftMouseBefore">是否在按下元素战技前先松开鼠标左键,避免输入冲突,默认 true</param>
/// <param name="releaseLeftMouseDelayMs">松开鼠标左键后的缓冲时间(毫秒),默认 10ms</param>
/// <param name="postKeyUpDelayMs">元素战技释放后的缓冲时间(毫秒),默认 50ms</param>
public static async Task SimulateHoldElementalSkillAsync(
int holdMs,
CancellationToken ct,
bool releaseLeftMouseBefore = true,
int releaseLeftMouseDelayMs = 10,
int postKeyUpDelayMs = 50)
{
if (releaseLeftMouseBefore)
{
Simulation.SendInput.Mouse.LeftButtonUp();
await Delay(releaseLeftMouseDelayMs, ct);
}
await SimulateHoldActionAsync(GIActions.ElementalSkill, holdMs, ct);
await Delay(postKeyUpDelayMs, ct);
}
/// <summary>
/// 模拟鼠标左键连续点击循环如万叶长E后的下落攻击。双层 try/finally 设计以确保无论在循环的哪个阶段发生取消或异常,鼠标左键都会被强制释放。
/// </summary>
/// <param name="repeatCount">需要循环点击的次数</param>
/// <param name="ct">用于监控任务取消的取消令牌</param>
/// <param name="preUpDelayMs">每次点击前,预先抬起左键后的缓冲延时(毫秒),默认 10ms</param>
/// <param name="downHoldMs">鼠标左键按下的保持时间(毫秒),默认 35ms</param>
/// <param name="postUpDelayMs">每次点击完成后的等待时间(毫秒),默认 50ms</param>
public static async Task SimulateMouseLeftClickLoopAsync(
int repeatCount,
CancellationToken ct,
int preUpDelayMs = 10,
int downHoldMs = 35,
int postUpDelayMs = 50)
{
try
{
for (var i = 0; i < repeatCount; i++)
{
Simulation.SendInput.Mouse.LeftButtonUp();
await Delay(preUpDelayMs, ct);
Simulation.SendInput.Mouse.LeftButtonDown();
try
{
await Delay(downHoldMs, ct);
}
finally
{
Simulation.SendInput.Mouse.LeftButtonUp();
}
await Delay(postUpDelayMs, ct);
}
}
finally
{
Simulation.SendInput.Mouse.LeftButtonUp();
}
}
public static Mat CaptureGameImage(IGameCapture? gameCapture)
{
var image = gameCapture?.Capture();
if (image == null)
{
Logger.LogWarning("截图失败!");
// 重试3次
for (var i = 0; i < 3; i++)
{
image = gameCapture?.Capture();
if (image != null)
{
return image;
}
Sleep(30);
}
throw new Exception("尝试多次后,截图失败!");
}
else
{
return image;
}
}
public static Mat? CaptureGameImageNoRetry(IGameCapture? gameCapture)
{
return gameCapture?.Capture();
}
/// <summary>
/// 自动判断当前运行上下文中截图方式,并选择合适的截图方式返回
/// </summary>
/// <returns></returns>
public static ImageRegion CaptureToRectArea(bool forceNew = false)
{
var image = CaptureGameImage(TaskTriggerDispatcher.GlobalGameCapture);
var content = new CaptureContent(image, 0, 0);
return content.CaptureRectArea;
}
}