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

239 lines
9.9 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.Threading;
using System.Threading.Tasks;
using BetterGenshinImpact.GameTask.AutoPathing.Model;
using Microsoft.Extensions.Logging;
using static BetterGenshinImpact.GameTask.Common.TaskControl;
using BetterGenshinImpact.GameTask.AutoFight.Model;
using BetterGenshinImpact.GameTask.AutoFight.Script;
using System.Linq;
using System.Collections.Generic;
using BetterGenshinImpact.GameTask.AutoFight.Config;
namespace BetterGenshinImpact.GameTask.AutoPathing.Handler;
/// <summary>
/// 使用万叶或琴团通过战技吸取拾取物品,优先万叶,如果没有万叶则使用琴团
/// </summary>
public class PickUpCollectHandler : IActionHandler
{
/// <summary>
/// 新增命令时,以角色名称开头(必填),“-”后定义动作(必填,用于预解析,不能单写角色名称),空格后定义参数(必填)
/// 1、"action": "pick_up_collect","action_params":为空或不填在队伍中寻找CharacterNames中第一个找到的角色找到就会执行PickUpActions第一个找到的相关角色命令。
/// 2、"action": "pick_up_collect","action_params":"琴"只填角色名称或别名会执行PickUpActions第一个找到的相关角色命令。
/// 3、"action": "pick_up_collect","action_params":"琴-短E"填了具体角色和动作直接找PickUpActions找到该命令执行。
/// </summary>
public static readonly string[] PickUpActions =
[
"枫原万叶-长E attack(0.08),keydown(E),wait(0.8),keyup(E),attack(0.5)",
"枫原万叶-短E attack(0.08),keydown(E),wait(0.47),keyup(E),attack(0.5)",
"琴-短E wait(0.1),keydown(E),wait(0.4),moveby(1000,0),wait(0.2),moveby(1000,0),wait(0.2),moveby(1000,0),wait(0.2),moveby(1000,-3500),wait(1.8),keyup(E),wait(0.3),click(middle)",
"琴-长E wait(0.1),click(middle),keydown(E),click(middle),wait(0.4),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1)," +
"moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1)," +
"moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1)," +
"moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1)," +
"moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1)," +
"moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1)," +
"moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1)," +
"moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1)," +
"moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),moveby(1000,3500),wait(1.8),keyup(E),wait(0.3),click(middle),wait(0.3)",
];
// 预解析所有角色名
private static readonly HashSet<string> CharacterNames = new HashSet<string>(
PickUpActions
.Select(action => action.Split(' ')[0])
.Select(GetBaseCharacterName)
.Distinct()
);
public async Task RunAsync(CancellationToken ct, WaypointForTrack? waypointForTrack = null, object? config = null)
{
Logger.LogInformation("简易策略:执行 {Nhd} 动作","聚集材料");
var combatScenes = await RunnerContext.Instance.GetCombatScenes(ct);
if (combatScenes == null)
{
Logger.LogError("队伍识别未初始化成功!");
return;
}
Avatar? picker = null;
var commandsList = new List<string>();
if (waypointForTrack != null)
{
if (!string.IsNullOrEmpty(waypointForTrack.ActionParams))
{
var commands = waypointForTrack.ActionParams.Split(',');
foreach (var command in commands)
{
try
{
var alias = DefaultAutoFightConfig.AvatarAliasToStandardName(command);
commandsList.Add(!string.IsNullOrEmpty(alias) ? alias : command);
}
catch (Exception e)
{
commandsList.Add(command);
Console.WriteLine(e);
}
}
}
else
{
// 1、ActionParams没填参数尝试选择如果找到后续会执行第一个找到该角色的相关命令
foreach (var characterName in CharacterNames)
{
var pickerNull = combatScenes.SelectAvatar(characterName);
if (pickerNull is null)
{
continue;
}
commandsList.Add(characterName);
break;
}
}
}
foreach (var commands in commandsList)
{
if (CharacterNames.Contains(commands))
{
picker = combatScenes.SelectAvatar(commands);
}
else
{
var characterName = GetCharacterName(commands);
picker = combatScenes.SelectAvatar(characterName);
}
if (picker is not null)
{
picker.TrySwitch();
await picker.WaitSkillCd(ct);
}
else
{
continue;
}
PickUpMaterial(combatScenes,commands); // 开始执行动作
}
}
/// <summary>
/// 执行聚集材料动作
/// <param name="combatScenes"></param>
/// <param name="pickerName"></param>
/// </summary>
private void PickUpMaterial(CombatScenes combatScenes, string? pickerName = null)
{
try
{
var foundAvatar = false;
string[] actionsToUse;
var characterName = string.Empty;
if (pickerName != null)
{
actionsToUse = PickUpActions.Where(action =>
action.StartsWith(pickerName + " ", StringComparison.OrdinalIgnoreCase)).ToArray();
if (actionsToUse.Length == 0)
{
if (CharacterNames.Contains(pickerName)) //2.只填了角色名则用基础角色名筛选执行pickerName相关的第一个命令
{
var actions = PickUpActions.FirstOrDefault(action => action.StartsWith(pickerName, StringComparison.OrdinalIgnoreCase));
actionsToUse = actions == null ? new string[0] : new string[] {actions};
// 替换第一个空格前的字符为 pickerName
if (actionsToUse.Length > 0 && actionsToUse[0].Contains(' '))
{
string action = actionsToUse[0];
int firstSpaceIndex = action.IndexOf(' ');
actionsToUse[0] = pickerName + action.Substring(firstSpaceIndex);
}
}
else
{
Logger.LogError($"未找到角色 {pickerName} 对应的动作");
return;
}
}
else
{
// 提取角色名称
characterName = GetCharacterName(pickerName);
// 3.填了具体命令,则用具体命令筛选,并将命令中的角色替换为角色名称
actionsToUse = actionsToUse
.Select(action => action.Replace(pickerName + " ", characterName + " ", StringComparison.OrdinalIgnoreCase))
.ToArray();
}
}
else
{
Logger.LogError("未找到ActionParams");
return;
}
foreach (var pickUpActionStr in actionsToUse)
{
var pickUpAction = CombatScriptParser.ParseContext(pickUpActionStr);
foreach (var command in pickUpAction.CombatCommands)
{
var avatar = combatScenes.SelectAvatar(command.Name);
if (avatar != null)
{
command.Execute(combatScenes);
foundAvatar = true;
}
}
if (foundAvatar)
{
var selectedAvatar = combatScenes.SelectAvatar(characterName);
if (selectedAvatar is not null)
{
Sleep(200);//等待CD显示
selectedAvatar.AfterUseSkill();
}
break;
}
}
}
catch (Exception ex)
{
// 处理异常
Console.WriteLine($"PickUpCollectHandler 异常: {ex.Message}");
}
}
// 直接匹配预解析的角色名
private static string GetCharacterName(string pickerName)
{
foreach (var name in CharacterNames)
{
if (pickerName.StartsWith(name))
return name;
}
return pickerName;
}
/// <summary>
/// 从完整动作名提取基础角色名
/// </summary>
private static string GetBaseCharacterName(string fullActionName)
{
// 找到第一个"-"号的位置
var dashIndex = fullActionName.IndexOf('-');
// 如果存在"-"号,则返回"-"号前的部分
return dashIndex > 0 ? fullActionName.Substring(0, dashIndex) : string.Empty;
}
}