Files
better-genshin-impact/BetterGenshinImpact/GameTask/AutoFishing/BehaviourTreeExtensions.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

222 lines
6.8 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.Composites;
using BehaviourTree.FluentBuilder;
using BehaviourTree;
using System;
using System.Collections.Generic;
using System.Text;
using BetterGenshinImpact.Core.Config;
using OpenCvSharp;
using System.IO;
using Microsoft.Extensions.Logging;
using BetterGenshinImpact.GameTask.Model.Area;
using System.Threading.Tasks;
using System.Linq;
namespace BetterGenshinImpact.GameTask.AutoFishing
{
public static class BehaviourTreeExtensions
{
public static FluentBuilder<TContext> MySimpleParallel<TContext>(this FluentBuilder<TContext> builder, string name, SimpleParallelPolicy policy = SimpleParallelPolicy.BothMustSucceed)
{
return builder.PushComposite((IBehaviour<TContext>[] children) => new MySimpleParallel<TContext>(name, policy, children));
}
}
/// <summary>
/// MySimpleParallel
/// 和SimpleParallel的区别是任一子行为返回失败则返回失败并且支持两个以上子行为
/// </summary>
/// <typeparam name="TContext"></typeparam>
public class MySimpleParallel<TContext> : CompositeBehaviour<TContext>
{
private readonly Func<TContext, BehaviourStatus> _behave;
public readonly SimpleParallelPolicy Policy;
public MySimpleParallel(SimpleParallelPolicy policy, IBehaviour<TContext>[] children)
: this("SimpleParallel", policy, children)
{
}
public MySimpleParallel(string name, SimpleParallelPolicy policy, IBehaviour<TContext>[] children)
: base(name, children)
{
Policy = policy;
_behave = ((policy == SimpleParallelPolicy.BothMustSucceed) ? new Func<TContext, BehaviourStatus>(BothMustSucceedBehaviour) : new Func<TContext, BehaviourStatus>(OnlyOneMustSucceedBehaviour));
}
private BehaviourStatus OnlyOneMustSucceedBehaviour(TContext context)
{
if (Children.Any(c => c.Status == BehaviourStatus.Succeeded))
{
return BehaviourStatus.Succeeded;
}
if (Children.All(c => c.Status == BehaviourStatus.Failed))
{
return BehaviourStatus.Failed;
}
return BehaviourStatus.Running;
}
private BehaviourStatus BothMustSucceedBehaviour(TContext context)
{
if (Children.All(c => c.Status == BehaviourStatus.Succeeded))
{
return BehaviourStatus.Succeeded;
}
if (Children.Any(c => c.Status == BehaviourStatus.Failed))
{
return BehaviourStatus.Failed;
}
return BehaviourStatus.Running;
}
protected override BehaviourStatus Update(TContext context)
{
if (base.Status != BehaviourStatus.Running)
{
foreach (var child in Children)
{
child.Tick(context);
}
}
else
{
foreach (var child in Children)
{
if (child.Status == BehaviourStatus.Ready || child.Status == BehaviourStatus.Running)
{
child.Tick(context);
}
}
}
if (Children.Any(c => c.Status == BehaviourStatus.Failed))
{
return BehaviourStatus.Failed;
}
else
{
return _behave(context);
}
}
protected override void DoReset(BehaviourStatus status)
{
base.DoReset(status);
}
}
/// <summary>
/// 原库的BaseBehaviour继承后也无法改写Tick方法只得另起此实现类
/// 暂时直接覆盖行为树原本的BaseBehaviour
/// </summary>
public abstract class BaseBehaviour<TImageRegion> : IBehaviour<TImageRegion>, IDisposable where TImageRegion : ImageRegion
{
protected readonly bool saveScreenshotOnTerminate;
protected readonly ILogger logger;
public string Name { get; }
public BehaviourStatus Status { get; private set; }
protected BaseBehaviour(string name, ILogger logger, bool saveScreenshotOnTerminate)
{
Name = name;
this.logger = logger;
this.saveScreenshotOnTerminate = saveScreenshotOnTerminate;
}
public BehaviourStatus Tick(TImageRegion context)
{
if (Status == BehaviourStatus.Ready)
{
OnInitialize();
}
Status = Update(context);
if (Status == BehaviourStatus.Ready)
{
throw new InvalidOperationException("Ready status should not be returned by Behaviour Update Method");
}
if (Status != BehaviourStatus.Running)
{
if (saveScreenshotOnTerminate)
{
string fileName = $"{DateTime.Now:yyyyMMddHHmmssfff}_{this.GetType().Name}_{Status}.png";
logger.LogInformation("保存截图: {Name}", fileName);
SaveScreenshot(context, fileName);
}
OnTerminate(Status);
}
return Status;
}
public virtual void SaveScreenshot(ImageRegion imageRegion, string? name)
{
var path = Global.Absolute($@"log\screenshot\");
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
var bitmap = imageRegion.SrcBitmap;
if (String.IsNullOrWhiteSpace(name))
{
name = $@"{DateTime.Now:yyyyMMddHHmmssffff}.png";
}
var savePath = Global.Absolute($@"log\screenshot\{name}");
var mat = imageRegion.SrcMat;
if (TaskContext.Instance().Config.CommonConfig.ScreenshotUidCoverEnabled)
{
var rect = TaskContext.Instance().Config.MaskWindowConfig.UidCoverRect;
mat.Rectangle(rect, Scalar.White, -1);
}
new Task(() =>
{
Cv2.ImWrite(savePath, mat);
}).Start();
}
public void Reset()
{
if (Status != 0)
{
DoReset(Status);
Status = BehaviourStatus.Ready;
}
}
protected abstract BehaviourStatus Update(TImageRegion context);
protected virtual void OnTerminate(BehaviourStatus status)
{
}
protected virtual void OnInitialize()
{
}
protected virtual void DoReset(BehaviourStatus status)
{
}
protected virtual void Dispose(bool disposing)
{
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}