Files
better-genshin-impact/BetterGenshinImpact/GameTask/AutoFight/AutoFightTask.cs
Takaranoao 399441b9e8 优化技能冷却处理逻辑 (#1321)
* 在路径追踪重构了部分冷却处理逻辑,战斗脚本e增加wait参数可等待技能冷却而不是跳过。采矿e增加等待。尝试修复路径追踪 UseElementalSkill 与采矿脚本冲突的问题。

* 给CombatCommand加入快速跳过e的选项

* 优化技能冷却处理逻辑,增加OcrSkillCd属性以支持OCR识别的技能冷却时间,并调整相关技能CD计算和等待逻辑,尝试修复纳西妲采集终止时按键未弹起的问题

* 优化战斗任务中的技能冷却处理逻辑

* 更新纳西妲技能冷却时间记录,改为使用UTC时间并增加日志输出以便调试

* 增加最大技能CD检查,以排除系统时间/日期同步导致无限卡死。修复跑图路切人。(ps:主板电池没电应该去修主板)

* 修复CheckAvatarAvailable

* fix AutoFightTask skill cooldown logic and improve comments

* 尝试修复脚本在"当前角色"下的小问题

* 尝试修复脚本在"当前角色"下的小问题,Avatar类结构调整,重新做了"根据技能cd优化出招"部分。

* Refactor avatar retrieval in PathingConditionConfig to use GetAvatars method and update skill cooldown references

* Fix variable naming for clarity in CombatScenes

* 在自动战斗执行前预先过滤不可执行的脚本。

---------

Co-authored-by: 辉鸭蛋 <huiyadanli@gmail.com>
2025-04-04 13:54:44 +08:00

627 lines
24 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.ONNX;
using BetterGenshinImpact.Core.Simulator;
using BetterGenshinImpact.Core.Simulator.Extensions;
using BetterGenshinImpact.GameTask.AutoFight.Model;
using BetterGenshinImpact.GameTask.AutoFight.Script;
using BetterGenshinImpact.GameTask.Model.Area;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using static BetterGenshinImpact.GameTask.Common.TaskControl;
using BetterGenshinImpact.GameTask.Common.Job;
using OpenCvSharp;
using BetterGenshinImpact.Helpers;
using Vanara;
namespace BetterGenshinImpact.GameTask.AutoFight;
public class AutoFightTask : ISoloTask
{
public string Name => "自动战斗";
private readonly AutoFightParam _taskParam;
private readonly CombatScriptBag _combatScriptBag;
private CancellationToken _ct;
private readonly BgiYoloV8Predictor _predictor;
private DateTime _lastFightFlagTime = DateTime.Now; // 战斗标志最近一次出现的时间
private readonly double _dpi = TaskContext.Instance().DpiScale;
private class TaskFightFinishDetectConfig
{
public int DelayTime = 1500;
public int DetectDelayTime = 450;
public Dictionary<string, int> DelayTimes = new();
public double CheckTime = 5;
public List<string> CheckNames = new();
public bool FastCheckEnabled;
public TaskFightFinishDetectConfig(AutoFightParam.FightFinishDetectConfig finishDetectConfig)
{
FastCheckEnabled = finishDetectConfig.FastCheckEnabled;
ParseCheckTimeString(finishDetectConfig.FastCheckParams, out CheckTime, CheckNames);
ParseFastCheckEndDelayString(finishDetectConfig.CheckEndDelay, out DelayTime, DelayTimes);
BattleEndProgressBarColor =
ParseStringToTuple(finishDetectConfig.BattleEndProgressBarColor, (95, 235, 255));
BattleEndProgressBarColorTolerance =
ParseSingleOrCommaSeparated(finishDetectConfig.BattleEndProgressBarColorTolerance, (6, 6, 6));
DetectDelayTime =
(int)((double.TryParse(finishDetectConfig.BeforeDetectDelay, out var result) ? result : 0.45) * 1000);
}
public (int, int, int) BattleEndProgressBarColor { get; }
public (int, int, int) BattleEndProgressBarColorTolerance { get; }
public static void ParseCheckTimeString(
string input,
out double checkTime,
List<string> names)
{
checkTime = 5;
if (string.IsNullOrEmpty(input))
{
return; // 直接返回
}
var uniqueNames = new HashSet<string>(); // 用于临时去重的集合
// 按分号分割字符串
var segments = input.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var segment in segments)
{
var trimmedSegment = segment.Trim();
// 如果是纯数字部分
if (double.TryParse(trimmedSegment, NumberStyles.Float, CultureInfo.InvariantCulture,
out double number))
{
checkTime = number; // 更新 CheckTime
}
else if (!uniqueNames.Contains(trimmedSegment)) // 如果是非数字且不重复
{
uniqueNames.Add(trimmedSegment); // 添加到集合
}
}
names.AddRange(uniqueNames); // 将集合转换为列表
}
public static void ParseFastCheckEndDelayString(
string input,
out int delayTime,
Dictionary<string, int> nameDelayMap)
{
delayTime = 1500;
if (string.IsNullOrEmpty(input))
{
return; // 直接返回
}
// 分割字符串,以分号为分隔符
var segments = input.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var segment in segments)
{
var parts = segment.Split(',');
// 如果是纯数字部分
if (parts.Length == 1)
{
if (double.TryParse(parts[0].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture,
out double number))
{
delayTime = (int)(number * 1000); // 更新 delayTime
}
}
// 如果是名字,数字格式
else if (parts.Length == 2)
{
string name = parts[0].Trim();
if (double.TryParse(parts[1].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture,
out double value))
{
nameDelayMap[name] = (int)(value * 1000); // 更新字典,取最后一个值
}
}
// 其他格式,跳过不处理
}
}
static bool IsSingleNumber(string input, out int result)
{
return int.TryParse(input, out result);
}
static (int, int, int) ParseSingleOrCommaSeparated(string input, (int, int, int) defaultValue)
{
// 如果是单个数字
if (IsSingleNumber(input, out var singleNumber))
{
return (singleNumber, singleNumber, singleNumber);
}
return ParseStringToTuple(input, defaultValue);
}
static (int, int, int) ParseStringToTuple(string input, (int, int, int) defaultValue)
{
// 尝试按逗号分割字符串
var parts = input.Split(',');
if (parts.Length == 3 &&
int.TryParse(parts[0], out var num1) &&
int.TryParse(parts[1], out var num2) &&
int.TryParse(parts[2], out var num3))
{
return (num1, num2, num3);
}
// 如果解析失败,返回默认值
return defaultValue;
}
}
private TaskFightFinishDetectConfig _finishDetectConfig;
public AutoFightTask(AutoFightParam taskParam)
{
_taskParam = taskParam;
_combatScriptBag = CombatScriptParser.ReadAndParse(_taskParam.CombatStrategyPath);
if (_taskParam.FightFinishDetectEnabled)
{
_predictor = BgiYoloV8PredictorFactory.GetPredictor(@"Assets\Model\World\bgi_world.onnx");
}
_finishDetectConfig = new TaskFightFinishDetectConfig(_taskParam.FinishDetectConfig);
}
// 方法1判断是否是单个数字
/*public int delayTime=1500;
public Dictionary<string, int> delayTimes = new();
public double checkTime = 5;
public List<string> checkNames = new();*/
public async Task Start(CancellationToken ct)
{
_ct = ct;
LogScreenResolution();
var combatScenes = new CombatScenes().InitializeTeam(CaptureToRectArea());
if (!combatScenes.CheckTeamInitialized())
{
throw new Exception("识别队伍角色失败");
}
// var actionSchedulerByCd = ParseStringToDictionary(_taskParam.ActionSchedulerByCd);
var combatCommands = _combatScriptBag.FindCombatScript(combatScenes.GetAvatars());
// 命令用到的角色 筛选交集
var commandAvatars = combatCommands.Select(c => c.Name).Distinct()
.Select(n => combatScenes.SelectAvatar(n))
.WhereNotNull().ToImmutableDictionary(avatar => avatar.Name);
// 过滤不可执行的脚本Task里并不支持"当前角色"。
combatCommands = combatCommands
.Where(c =>commandAvatars.ContainsKey(c.Name))
.ToList();
if (commandAvatars.IsEmpty)
{
throw new Exception("没有可用战斗脚本");
}
// 可以跳过的角色名
List<string> canBeSkippedAvatarNames = [];
// 通过环境更新角色的cd并看看那些角色启用了跳过
foreach (var avatar in commandAvatars)
{
var mCd = Avatar.ParseActionSchedulerByCd(_taskParam.ActionSchedulerByCd, avatar.Key);
if (mCd is not null)
{
avatar.Value.ManualSkillCd = (double)mCd;
canBeSkippedAvatarNames.Add(avatar.Key);
}
else
{
avatar.Value.ManualSkillCd = -1;
}
}
// 新的取消token
var cts2 = new CancellationTokenSource();
ct.Register(cts2.Cancel);
combatScenes.BeforeTask(cts2.Token);
TimeSpan fightTimeout = TimeSpan.FromSeconds(_taskParam.Timeout); // 战斗超时时间
Stopwatch timeoutStopwatch = Stopwatch.StartNew();
Stopwatch checkFightFinishStopwatch = Stopwatch.StartNew();
TimeSpan checkFightFinishTime = TimeSpan.FromSeconds(_finishDetectConfig.CheckTime); //检查战斗超时时间的超时时间
//战斗前检查,可做成配置
// if (await CheckFightFinish()) {
// return;
// }
var fightEndFlag = false;
string lastFightName = "";
//统计切换人打架次数
var countFight = 0;
// 战斗操作
var fightTask = Task.Run(async () =>
{
try
{
while (!cts2.Token.IsCancellationRequested)
{
if (commandAvatars.Count == canBeSkippedAvatarNames.Count)
{
// 所有战斗角色都可以被取消
var minCoolDown = commandAvatars.Select(a => a.Value.GetSkillCdSeconds()).Min();
if (minCoolDown > 0)
{
Logger.LogInformation("队伍中所有角色的技能都在冷却中,等待{MinCoolDown}秒后继续。", Math.Round(minCoolDown, 2));
await Delay((int)Math.Ceiling(minCoolDown * 1000), ct);
}
}
var skipFightName = "";
// 通用化战斗策略
for (var i = 0; i < combatCommands.Count; i++)
{
var command = combatCommands[i];
//如果上个执行者和这次是一个角色且中间没有跳过继续执行不判断cd
if (lastFightName != command.Name || skipFightName != "")
{
var avatar = combatScenes.SelectAvatar(command.Name);
if (avatar is null)
{
continue;
}
var cd = avatar.GetSkillCdSeconds();
if (cd > 0)
{
if (skipFightName != command.Name && canBeSkippedAvatarNames.Contains(command.Name))
{
var manualSkillCd = avatar.ManualSkillCd;
if (manualSkillCd > 0)
{
Logger.LogInformation("{commandName}cd冷却为{skillCd}秒,剩余{Cd}秒,跳过此次行动",
command.Name,
manualSkillCd, Math.Round(cd, 2));
}
else
{
Logger.LogInformation("{CommandName}cd冷却剩余{Cd}秒,跳过此次行动", command.Name,
Math.Round(cd, 2));
}
}
// 避免重复log提示
skipFightName = command.Name;
continue;
}
skipFightName = "";
}
if (timeoutStopwatch.Elapsed > fightTimeout)
{
Logger.LogInformation("战斗超时结束");
fightEndFlag = true;
break;
}
command.Execute(combatScenes);
//统计战斗人次
if (i == combatCommands.Count - 1 || command.Name != combatCommands[i + 1].Name)
{
countFight++;
}
lastFightName = command.Name;
if (!fightEndFlag && _taskParam is { FightFinishDetectEnabled: true })
{
//处于最后一个位置,或者当前执行人和下一个人名字不一样的情况,满足一定条件(开启快速检查并且检查时间大于0或人名存在配置)检查战斗
if (i == combatCommands.Count - 1
|| (
_finishDetectConfig.FastCheckEnabled &&
command.Name != combatCommands[i + 1].Name &&
((_finishDetectConfig.CheckTime > 0 &&
checkFightFinishStopwatch.Elapsed > checkFightFinishTime)
|| _finishDetectConfig.CheckNames.Contains(command.Name))
))
{
checkFightFinishStopwatch.Restart();
var delayTime = _finishDetectConfig.DelayTime;
var detectDelayTime = _finishDetectConfig.DetectDelayTime;
if (_finishDetectConfig.DelayTimes.TryGetValue(command.Name, out var time))
{
delayTime = time;
Logger.LogInformation($"{command.Name}结束后,延时检查为{delayTime}毫秒");
}
else
{
Logger.LogInformation($"延时检查为{delayTime}毫秒");
}
/*if (i<combatCommands.Count - 1)
{
Logger.LogInformation($"{command.Name}下一个人为{combatCommands[i+1].Name}毫秒");
}*/
fightEndFlag = await CheckFightFinish(delayTime, detectDelayTime);
}
}
if (fightEndFlag)
{
break;
}
}
if (fightEndFlag)
{
break;
}
}
}
catch (Exception e)
{
Debug.WriteLine(e.Message);
Debug.WriteLine(e.StackTrace);
throw;
}
finally
{
Simulation.ReleaseAllKey();
}
}, cts2.Token);
await fightTask;
// 战斗结束检测线程
// var endTask = Task.Run(async () =>
// {
// if (!_taskParam.FightFinishDetectEnabled)
// {
// return;
// }
//
// try
// {
// while (!cts2.IsCancellationRequested)
// {
// var finish = await CheckFightFinish();
// if (finish)
// {
// await cts2.CancelAsync();
// break;
// }
//
// Sleep(1000, cts2.Token);
// }
// }
// catch (Exception e)
// {
// Debug.WriteLine(e.Message);
// Debug.WriteLine(e.StackTrace);
// }
// }, cts2.Token);
//
// await Task.WhenAll(fightTask, endTask);
if (_taskParam.KazuhaPickupEnabled)
{
// 队伍中存在万叶的时候使用一次长E
var kazuha = combatScenes.SelectAvatar("枫原万叶");
if (kazuha != null)
{
var time = DateTime.UtcNow - kazuha.LastSkillTime;
//当万叶cd大于3时或战斗人次少于2时通常无怪物情况下此时不再触发万叶拾取
if (!(countFight < 2 || lastFightName == "枫原万叶" && time.TotalSeconds > 3))
{
Logger.LogInformation("使用枫原万叶长E拾取掉落物");
await Delay(300, ct);
if (kazuha.TrySwitch())
{
await kazuha.WaitSkillCd(ct);
kazuha.UseSkill(true);
await Task.Delay(100);
Simulation.SendInput.SimulateAction(GIActions.NormalAttack);
await Delay(1500, ct);
}
}
else
{
Logger.LogInformation((countFight < 2 ? "首个人出招就结束战斗,应该无怪物" : "距最近一次万叶出招,时间过短") + ",跳过此次万叶拾取!");
}
}
}
if (_taskParam is { PickDropsAfterFightEnabled: true })
{
// 执行自动拾取掉落物的功能
await new ScanPickTask().Start(ct);
}
}
private void LogScreenResolution()
{
AssertUtils.CheckGameResolution("自动战斗");
}
static bool AreDifferencesWithinBounds((int, int, int) a, (int, int, int) b, (int, int, int) c)
{
// 计算每个位置的差值绝对值并进行比较
return Math.Abs(a.Item1 - b.Item1) < c.Item1 &&
Math.Abs(a.Item2 - b.Item2) < c.Item2 &&
Math.Abs(a.Item3 - b.Item3) < c.Item3;
}
private async Task<bool> CheckFightFinish(int delayTime = 1500, int detectDelayTime = 450)
{
// YOLO 判断血条和怪物位置
// if (HasFightFlagByYolo(CaptureToRectArea()))
// {
// _lastFightFlagTime = DateTime.Now;
// return false;
// }
//
//Random random = new Random();
//double randomFraction = random.NextDouble(); // 生成 0 到 1 之间的随机小数
//此处随机数防止固定招式下使按L正好处于招式下导致无法准确判断战斗结束
// double randomNumber = 1 + (randomFraction * (3 - 1));
// 几秒内没有检测到血条和怪物位置,则开始旋转视角重新检测
//if ((DateTime.Now - _lastFightFlagTime).TotalSeconds > randomNumber)
//{
// 旋转完毕后都没有检测到血条和怪物位置则按L键确认战斗结束
/**
Simulation.SendInput.Mouse.MiddleButtonClick();
await Delay(300, _ct);
for (var i = 0; i < 8; i++)
{
Simulation.SendInput.Mouse.MoveMouseBy((int)(500 * _dpi), 0);
await Delay(800, _ct); // 等待视角稳定
if (HasFightFlagByYolo(CaptureToRectArea()))
{
_lastFightFlagTime = DateTime.Now;
return false;
}
}
**/
//Simulation.SendInput.SimulateAction(GIActions.Drop);//在换队前取消爬墙状态
await Delay(delayTime, _ct);
Logger.LogInformation("打开编队界面检查战斗是否结束,延时{detectDelayTime}毫秒检查", detectDelayTime);
// 最终方案确认战斗结束
Simulation.SendInput.SimulateAction(GIActions.OpenPartySetupScreen);
await Delay(detectDelayTime, _ct);
var ra = CaptureToRectArea();
var b3 = ra.SrcMat.At<Vec3b>(50, 790); //进度条颜色
var whiteTile = ra.SrcMat.At<Vec3b>(50, 768); //白块
Simulation.SendInput.SimulateAction(GIActions.Drop);
if (IsWhite(whiteTile.Item2, whiteTile.Item1, whiteTile.Item0) &&
IsYellow(b3.Item2, b3.Item1,
b3.Item0) /* AreDifferencesWithinBounds(_finishDetectConfig.BattleEndProgressBarColor, (b3.Item0, b3.Item1, b3.Item2), _finishDetectConfig.BattleEndProgressBarColorTolerance)*/
)
{
Logger.LogInformation("识别到战斗结束");
return true;
}
Logger.LogInformation($"未识别到战斗结束yellow{b3.Item0},{b3.Item1},{b3.Item2}");
Logger.LogInformation($"未识别到战斗结束white{whiteTile.Item0},{whiteTile.Item1},{whiteTile.Item2}");
/**
if (!Bv.IsInMainUi(ra))
{
// 如果不在主界面,说明异常,直接结束战斗继续下一步(地图追踪下一步会进入异常处理)
Logger.LogInformation("当前不在主界面,直接结束战斗!");
return true;
}**/
_lastFightFlagTime = DateTime.Now;
return false;
}
bool IsYellow(int r, int g, int b)
{
//Logger.LogInformation($"IsYellow({r},{g},{b})");
// 黄色范围R高G高B低
return (r >= 200 && r <= 255) &&
(g >= 200 && g <= 255) &&
(b >= 0 && b <= 100);
}
bool IsWhite(int r, int g, int b)
{
//Logger.LogInformation($"IsWhite({r},{g},{b})");
// 白色范围R高G高B低
return (r >= 240 && r <= 255) &&
(g >= 240 && g <= 255) &&
(b >= 240 && b <= 255);
}
static double FindMax(double[] numbers)
{
if (numbers == null || numbers.Length == 0)
{
throw new ArgumentException("The array is empty or null.");
}
double max = numbers[0] > 10000 ? 0 : numbers[0];
foreach (var num in numbers)
{
var cpnum = numbers[0] > 10000 ? 0 : num;
max = Math.Max(max, num);
}
return max;
}
[Obsolete]
private static Dictionary<string, double> ParseStringToDictionary(string input, double defaultValue = -1)
{
var dictionary = new Dictionary<string, double>();
if (string.IsNullOrEmpty(input))
{
return dictionary; // 返回空字典
}
string[] pairs = input.Split(';', StringSplitOptions.RemoveEmptyEntries);
foreach (var pair in pairs)
{
var parts = pair.Split(',', StringSplitOptions.TrimEntries);
if (parts.Length > 0)
{
string name = parts[0];
double value = defaultValue;
if (parts.Length > 1 && double.TryParse(parts[1], out var parsedValue))
{
value = parsedValue;
}
dictionary[name] = value;
}
}
return dictionary;
}
private bool HasFightFlagByYolo(ImageRegion imageRegion)
{
// if (RuntimeHelper.IsDebug)
// {
// imageRegion.SrcMat.SaveImage(Global.Absolute(@"log\fight\" + $"{DateTime.Now:yyyyMMdd_HHmmss_ffff}.png"));
// }
var dict = _predictor.Detect(imageRegion);
return dict.ContainsKey("health_bar") || dict.ContainsKey("enemy_identify");
}
// 无用
// [Obsolete]
// private bool HasFightFlagByGadget(ImageRegion imageRegion)
// {
// // 小道具位置 1920-133,800,60,50
// var gadgetMat = imageRegion.DeriveCrop(AutoFightAssets.Instance.GadgetRect).SrcMat;
// var list = ContoursHelper.FindSpecifyColorRects(gadgetMat, new Scalar(225, 220, 225), new Scalar(255, 255, 255));
// // 要大于 gadgetMat 的 1/2
// return list.Any(r => r.Width > gadgetMat.Width / 2 && r.Height > gadgetMat.Height / 2);
// }
}