mirror of
https://github.com/babalae/better-genshin-impact.git
synced 2026-05-23 09:55:48 +08:00
* 修改万叶的模拟战技与普攻输入操作 将高层的技能释放 / 普通攻击函数调用,替换为明确的模拟输入时序流程,以提升运行稳定性。 改动内容: 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>
314 lines
10 KiB
C#
314 lines
10 KiB
C#
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;
|
||
}
|
||
}
|