Files
better-genshin-impact/BetterGenshinImpact/GameTask/AutoFishing/AutoFishingTrigger.cs
FishmanTheMurloc 57d33c4312 又一波钓鱼优化 (#1301)
* GetFishBarRect方法添加更复杂的算法,并为其配备独立的单元测试,和分离难度较大的测试用例(未熟练时两侧出现黄色动态折线的情况);GetFishBoxArea行为去掉拉条框初始位置必须位于屏幕中轴线的条件,并添加其后续Fishing行为的单元测试来验证可行性;EnterFishingMode行为使用结束时间来代替Sleep,并添加整体超时时间;添加一个鱼咬钩的假阳性测试用例仅供娱乐

* 补充GetFishBarRect算法,使通过遗漏的测试"20250314002439020_Fishing_Succeeded.png"

* 拉条增加1秒未检测持续时间以应对瞬间丢失拉条框的情况;新增一个检查提竿结果的行为;新增一个检查开始钓一条鱼的初始状态的方法,以应对行为状态错配的情况;一些行为将Sleep优化为DateTime;修改上述改动对应的单元测试

* 解决合并冲突剩余问题,删掉ImageRegion的Bitmap构造函数重载

* 提供给测试用例初始化的 SystemInfo、TaskContext 方法,使用 InitForTest 即可

* InitForTest

* 和鸭蛋昨夜的提交撞车了。。。抽象了ISystemInto供单元测试实例化Fake类;给BaseAssets类定义了成员字段systemInfo(我想,既然都是图片模板数据集,如此定义是合理的),供继承类AutoFishingAssets使用,并定义了其在单元测试的派生类;添加了一个900p的选取鱼饵测试用例;blackboard改为负责携带AutoFishingAssets,并将其实例化时机挪到独立任务的Start方法中,避免由于TaskContext尚未初始化导致获取到的SystemInfo为空

* 一个特殊的测试用例:抛竿的瞬间、开始检测咬杆时遇到了假阳性

* Revert "InitForTest"

This reverts commit 225e9783a7.

* Revert "提供给测试用例初始化的 SystemInfo、TaskContext 方法,使用 InitForTest 即可"

This reverts commit 610c57263a.

* 为始终没有找到落点的情况添加计数,在第3次时直接退出,并添加此情况的单元测试

---------

Co-authored-by: 辉鸭蛋 <huiyadanli@gmail.com>
2025-03-18 19:51:42 +08:00

360 lines
14 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 BehaviourTree;
using BehaviourTree.FluentBuilder;
using BehaviourTree.Composites;
using BetterGenshinImpact.Core.Simulator;
using BetterGenshinImpact.GameTask.AutoFishing.Assets;
using BetterGenshinImpact.GameTask.Common;
using Microsoft.Extensions.Logging;
using OpenCvSharp;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
using Point = OpenCvSharp.Point;
using Fischless.WindowsInput;
using BetterGenshinImpact.GameTask.Model.Area;
using BetterGenshinImpact.Core.Config;
using BetterGenshinImpact.Core.Recognition.ONNX;
using Compunet.YoloV8;
namespace BetterGenshinImpact.GameTask.AutoFishing
{
public class AutoFishingTrigger : ITaskTrigger
{
private readonly ILogger<AutoFishingTrigger> _logger = App.GetLogger<AutoFishingTrigger>();
private readonly InputSimulator input = Simulation.SendInput;
public string Name => "自动钓鱼";
public bool IsEnabled { get; set; }
public int Priority => 15;
/// <summary>
/// 钓鱼是要独占模式的
/// 在钓鱼的时候,不应该有其他任务在执行
/// 在触发器发现正在钓鱼的时候,启用独占模式
/// </summary>
public bool IsExclusive { get; set; }
private Blackboard blackboard;
/// <summary>
/// 辣条(误)
/// </summary>
private IBehaviour<ImageRegion> BehaviourTreeLaTiao { get; set; }
public AutoFishingTrigger()
{
var predictor = YoloV8Builder.CreateDefaultBuilder().UseOnnxModel(Global.Absolute(@"Assets\Model\Fish\bgi_fish.onnx")).WithSessionOptions(BgiSessionOption.Instance.Options).Build();
this.blackboard = new Blackboard(predictor, this.Sleep, AutoFishingAssets.Instance);
BehaviourTreeLaTiao = FluentBuilder.Create<ImageRegion>()
.MySimpleParallel("root", policy: SimpleParallelPolicy.OnlyOneMustSucceed)
.Do("检查是否在钓鱼界面", CheckFishingUserInterface)
.UntilSuccess("拉条循环")
.Sequence("拉条")
.PushLeaf(() => new FishBite("自动提竿", blackboard, _logger, false, input))
.PushLeaf(() => new GetFishBoxArea("等待拉条出现", blackboard, _logger, false))
.PushLeaf(() => new Fishing("钓鱼拉条", blackboard, _logger, false, input))
.End()
.End()
.End()
.Build();
}
public void Init()
{
IsEnabled = TaskContext.Instance().Config.AutoFishingConfig.Enabled;
IsExclusive = false;
}
private DateTime _prevExecute = DateTime.MinValue;
public void OnCapture(CaptureContent content)
{
if ((DateTime.Now - _prevExecute).TotalMilliseconds <= 67)
{
return;
}
_prevExecute = DateTime.Now;
// 进入独占的判定
if (!IsExclusive)
{
// 进入独占模式判断
CheckFishingUserInterface(content.CaptureRectArea);
}
else
{
// if (TaskContext.Instance().Config.AutoFishingConfig.AutoThrowRodEnabled)
// {
// BehaviourTree.Tick(content);
// }
// else
// {
// BehaviourTreeLaTiao.Tick(content);
// }
BehaviourTreeLaTiao.Tick(content.CaptureRectArea);
}
}
// /// <summary>
// /// 在“开始钓鱼”按钮上方安排一个我们的“开始自动钓鱼”按钮
// /// 点击按钮进入独占模式
// /// </summary>
// /// <param name="content"></param>
// /// <returns></returns>
// [Obsolete]
// private void DisplayButtonOnStartFishPageForExclusive(CaptureContent content)
// {
// VisionContext.Instance().DrawContent.RemoveRect("StartFishingButton");
// var info = TaskContext.Instance().SystemInfo;
// var srcMat = content.CaptureRectArea.SrcMat;
// var rightBottomMat = CropHelper.CutRightBottom(srcMat, srcMat.Width / 2, srcMat.Height / 2);
// var list = CommonRecognition.FindGameButton(rightBottomMat);
// if (list.Count > 0)
// {
// foreach (var rect in list)
// {
// var ro = new RecognitionObject()
// {
// Name = "StartFishingText",
// RecognitionType = RecognitionTypes.OcrMatch,
// RegionOfInterest = new Rect(srcMat.Width / 2, srcMat.Height / 2, srcMat.Width - srcMat.Width / 2,
// srcMat.Height - srcMat.Height / 2),
// AllContainMatchText = new List<string>
// {
// "开始", "钓鱼"
// },
// DrawOnWindow = false
// };
// var ocrRaRes = content.CaptureRectArea.Find(ro);
// if (ocrRaRes.IsEmpty())
// {
// WeakReferenceMessenger.Default.Send(new PropertyChangedMessage<object>(this, "RemoveButton", new object(), "开始自动钓鱼"));
// }
// else
// {
// VisionContext.Instance().DrawContent.PutRect("StartFishingButton", rect.ToWindowsRectangleOffset(srcMat.Width / 2, srcMat.Height / 2).ToRectDrawable());
//
// var btnPosition = new Rect(rect.X + srcMat.Width / 2, rect.Y + srcMat.Height / 2 - rect.Height - 10, rect.Width, rect.Height);
// var maskButton = new MaskButton("开始自动钓鱼", btnPosition, () =>
// {
// VisionContext.Instance().DrawContent.RemoveRect("StartFishingButton");
// _logger.LogInformation("→ {Text}", "自动钓鱼,启动!");
// // 点击下面的按钮
// var rc = info.CaptureAreaRect;
// Simulation.SendInputEx
// .Mouse
// .MoveMouseTo(
// (rc.X + srcMat.Width * 1d / 2 + rect.X + rect.Width * 1d / 2) * 65535 / info.DesktopRectArea.Width,
// (rc.Y + srcMat.Height * 1d / 2 + rect.Y + rect.Height * 1d / 2) * 65535 / info.DesktopRectArea.Height)
// .LeftButtonClick();
// WeakReferenceMessenger.Default.Send(new PropertyChangedMessage<object>(this, "RemoveButton", new object(), "开始自动钓鱼"));
// // 启动要延时一会等待钓鱼界面切换
// Sleep(1000);
// IsExclusive = true;
// _switchBaitContinuouslyFrameNum = 0;
// _waitBiteContinuouslyFrameNum = 0;
// _noFishActionContinuouslyFrameNum = 0;
// _isThrowRod = false;
// });
// WeakReferenceMessenger.Default.Send(new PropertyChangedMessage<object>(this, "AddButton", new object(), maskButton));
// }
// }
// }
// else
// {
// WeakReferenceMessenger.Default.Send(new PropertyChangedMessage<object>(this, "RemoveButton", new object(), "开始自动钓鱼"));
// }
// }
//private bool OcrStartFishingForExclusive(CaptureContent content)
//{
// var srcMat = content.CaptureRectArea.SrcMat;
// var rightBottomMat = CutHelper.CutRightBottom(srcMat, srcMat.Width / 2, srcMat.Height / 2);
// var text = _ocrService.Ocr(rightBottomMat.ToBitmap());
// if (!string.IsNullOrEmpty(text) && StringUtils.RemoveAllSpace(text).Contains("开始") && StringUtils.RemoveAllSpace(text).Contains("钓鱼"))
// {
// return true;
// }
// return false;
//}
/// <summary>
/// 钓鱼有3种场景
/// 1. 未抛竿 BaitButtonRo存在 && WaitBiteButtonRo不存在
/// 2. 抛竿后未拉条 WaitBiteButtonRo存在 && BaitButtonRo不存在
/// 3. 上钩拉条
///
/// 新AI钓鱼
/// 前提:必须要正面面对鱼塘,没有识别到鱼的时候不会自动抛竿
/// 1. 观察周围环境,判断鱼塘位置,视角对上鱼塘位置中心
/// 2. 根据第一步的观察结果,提前选择鱼饵
/// </summary>
[Obsolete]
private (int, int) MoveMouseToFish(Rect rect1, Rect rect2)
{
int minDistance;
//首先计算两个矩形中心点
Point c1, c2;
c1.X = rect1.X + (rect1.Width / 2);
c1.Y = rect1.Y + (rect1.Height / 2);
c2.X = rect2.X + (rect2.Width / 2);
c2.Y = rect2.Y + (rect2.Height / 2);
// 分别计算两矩形中心点在X轴和Y轴方向的距离
var dx = Math.Abs(c2.X - c1.X);
var dy = Math.Abs(c2.Y - c1.Y);
//两矩形不相交在X轴方向有部分重合的两个矩形
if (dx < (rect1.Width + rect2.Width) / 2 && dy >= (rect1.Height + rect2.Height) / 2)
{
minDistance = dy - ((rect1.Height + rect2.Height) / 2);
var moveY = 5;
if (minDistance >= 100)
{
moveY = 50;
}
if (c1.Y > c2.Y)
{
moveY = -moveY;
}
//_logger.LogInformation("移动鼠标 {X} {Y}", 0, moveY);
Simulation.SendInput.Mouse.MoveMouseBy(0, moveY);
return (0, minDistance);
}
//两矩形不相交在Y轴方向有部分重合的两个矩形
else if (dx >= (rect1.Width + rect2.Width) / 2 && (dy < (rect1.Height + rect2.Height) / 2))
{
minDistance = dx - ((rect1.Width + rect2.Width) / 2);
var moveX = 10;
if (minDistance >= 100)
{
moveX = 50;
}
if (c1.X > c2.X)
{
moveX = -moveX;
}
//_logger.LogInformation("移动鼠标 {X} {Y}", moveX, 0);
Simulation.SendInput.Mouse.MoveMouseBy(moveX, 0);
return (minDistance, 0);
}
//两矩形不相交在X轴和Y轴方向无重合的两个矩形
else if ((dx >= ((rect1.Width + rect2.Width) / 2)) && (dy >= ((rect1.Height + rect2.Height) / 2)))
{
var dpX = dx - ((rect1.Width + rect2.Width) / 2);
var dpY = dy - ((rect1.Height + rect2.Height) / 2);
//minDistance = (int)Math.Sqrt(dpX * dpX + dpY * dpY);
var moveX = 10;
if (dpX >= 100)
{
moveX = 50;
}
var moveY = 5;
if (dpY >= 100)
{
moveY = 50;
}
if (c1.Y > c2.Y)
{
moveY = -moveY;
}
if (c1.X > c2.X)
{
moveX = -moveX;
}
//_logger.LogInformation("移动鼠标 {X} {Y}", moveX, moveY);
Simulation.SendInput.Mouse.MoveMouseBy(moveX, moveY);
return (dpX, dpY);
}
//两矩形相交
else
{
//_logger.LogInformation("无需移动鼠标");
minDistance = -1;
return (0, 0);
}
}
public void Sleep(int millisecondsTimeout)
{
TaskControl.Sleep(millisecondsTimeout);
}
/// <summary>
/// 检查是否在钓鱼界面
/// 方法是找右下角的退出钓鱼按钮
/// 进入钓鱼界面时该触发器进入独占模式
/// </summary>
/// <param name="imageRegion"></param>
private BehaviourStatus CheckFishingUserInterface(ImageRegion imageRegion)
{
if (blackboard.chooseBaitUIOpening)
{
return BehaviourStatus.Running;
}
var prevIsExclusive = IsExclusive;
IsExclusive = !imageRegion.Find(AutoFishingAssets.Instance.ExitFishingButtonRo).IsEmpty();
if (IsExclusive)
{
if (IsEnabled && !prevIsExclusive)
{
_logger.LogInformation("→ {Text}", "半自动钓鱼,启动!");
// _logger.LogInformation("当前自动选饵抛竿状态[{Enabled}]", TaskContext.Instance().Config.AutoFishingConfig.AutoThrowRodEnabled.ToChinese());
}
return BehaviourStatus.Running;
}
else
{
if (prevIsExclusive)
{
_logger.LogInformation("← {Text}", "退出钓鱼界面");
}
return BehaviourStatus.Failed;
}
}
///// <summary>
///// 清理画布
///// </summary>
//public void ClearDraw()
//{
// VisionContext.Instance().DrawContent.PutOrRemoveRectList(new List<(string, RectDrawable)>
// {
// ("FishingBarLeft", new RectDrawable(System.Windows.Rect.Empty)),
// ("FishingBarCur", new RectDrawable(System.Windows.Rect.Empty)),
// ("FishingBarRight", new RectDrawable(System.Windows.Rect.Empty))
// });
// VisionContext.Instance().DrawContent.RemoveRect("FishBiteTips");
// VisionContext.Instance().DrawContent.RemoveRect("StartFishingButton");
// WeakReferenceMessenger.Default.Send(new PropertyChangedMessage<object>(this, "RemoveButton", new object(), "开始自动钓鱼"));
//}
//public void Stop()
//{
// ClearDraw();
//}
}
}