using System;
using System.IO;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BetterGenshinImpact.Core.Config;
using BetterGenshinImpact.Core.Recognition.ONNX;
using BetterGenshinImpact.Core.Simulator;
using BetterGenshinImpact.Core.Simulator.Extensions;
using BetterGenshinImpact.View.Drawable;
using Compunet.YoloSharp;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using BetterGenshinImpact.GameTask.Model.Area;
using OpenCvSharp;
using Vanara.PInvoke;
using static BetterGenshinImpact.Core.Simulator.Extensions.SimulateKeyHelper;
using static BetterGenshinImpact.GameTask.Common.TaskControl;
namespace BetterGenshinImpact.GameTask.Common.Job;
///
/// 莉奈娅挖矿
///
public class LinneaMiningTask
{
#region 配置参数
// 聚类距离阈值(基于宽度缩放)
private const double BaseClusterDistance = 400;
// 聚类面积基准值(1920宽度下的标准矿石面积)
private const double BaseClusterArea = 1800;
// 对准判定:使用目标矿物框四周扩张像素(基于宽度缩放)
private const double BaseAlignmentExpansion = 3;
// 屏幕边缘忽略区域宽度(基于宽度缩放)
private const double BaseEdgeIgnore = 200;
// 瞄准模式X轴灵敏度补偿系数
private const double AimSensitivityFactorX = 0.45;
// 瞄准模式Y轴灵敏度补偿系数
private const double AimSensitivityFactorY = 0.80;
// 检测置信度阈值
private const float ConfidenceThreshold = 0.70f;
// 聚类面积差异倍率
private const double AreaRatioThreshold = 4;
// 左转步长
private const int LeftTurnStep = -250;
// 内层最大检测次数
private const int MaxInnerRetry = 7;
// 默认射箭次数
public const int DefaultMineCount = 1;
// 默认大循环次数
public const int DefaultScanRounds = 1;
// 元素视野刷新间隔
private const int ElementSightRefreshMs = 3000;
#endregion
private readonly BgiYoloPredictor _predictor;
private readonly double _dpi = TaskContext.Instance().DpiScale;
private readonly double _widthScale = TaskContext.Instance().SystemInfo.CaptureAreaRect.Width / 1920.0;
private readonly double _heightScale = TaskContext.Instance().SystemInfo.CaptureAreaRect.Height / 1080.0;
private readonly double ClusterDistanceThreshold;
private readonly double EdgeIgnore;
private readonly double AlignmentExpansion;
private readonly int _scanRounds;
private readonly int _mineCount;
private readonly bool _preferRight;
private int _debugIndex;
public LinneaMiningTask(int scanRounds = DefaultScanRounds, int mineCount = DefaultMineCount)
{
_scanRounds = scanRounds;
_mineCount = mineCount;
_preferRight = scanRounds > 1;
_predictor = App.ServiceProvider.GetRequiredService()
.CreateYoloPredictor(BgiOnnxModel.BgiMine);
ClusterDistanceThreshold = BaseClusterDistance * _widthScale;
EdgeIgnore = BaseEdgeIgnore * _widthScale;
AlignmentExpansion = BaseAlignmentExpansion * _widthScale;
}
public async Task Start(CancellationToken ct)
{
var aimingModeEntered = false;
try
{
// Logger.LogInformation("开始寻矿");
Simulation.SendInput.Keyboard.KeyPress(GIActions.SwitchAimingMode.ToActionKey().ToVK());
aimingModeEntered = true;
await Delay(400, ct);
var minedCount = 0;
for (var round = 0; round < _scanRounds && !ct.IsCancellationRequested; round++)
{
Simulation.SendInput.Mouse.MiddleButtonDown();
await Delay(1500, ct);
_lastRefreshTime = Environment.TickCount64;
var (cluster, centerX, centerY) = FindNearestMineralCluster();
if (cluster != null)
{
var (aligned, counted, compensateDx, compensateDy) = await AlignAndMine(cluster, centerX, centerY, ct);
if (aligned)
{
if (counted) minedCount++;
if (minedCount >= _mineCount) break;
continue;
}
Simulation.SendInput.Mouse.MiddleButtonUp();
await Delay(300, ct);
if (compensateDx != 0 || compensateDy != 0)
{
Simulation.SendInput.Mouse.MiddleButtonDown();
await Delay(1500, ct);
_lastRefreshTime = Environment.TickCount64;
Simulation.SendInput.Mouse.MoveMouseBy(-compensateDx, -compensateDy);
await Delay(800, ct);
Simulation.SendInput.Mouse.MiddleButtonUp();
await Delay(300, ct);
}
if (round < _scanRounds - 1)
{
Simulation.SendInput.Mouse.MoveMouseBy((int)(LeftTurnStep * _dpi * _widthScale), 0);
await Delay(800, ct);
}
continue;
}
Simulation.SendInput.Mouse.MiddleButtonUp();
await Delay(300, ct);
if (round < _scanRounds - 1)
{
Simulation.SendInput.Mouse.MoveMouseBy((int)(LeftTurnStep * _dpi * _widthScale), 0);
}
await Delay(800, ct);
}
Simulation.SendInput.Keyboard.KeyPress(GIActions.SwitchAimingMode.ToActionKey().ToVK());
aimingModeEntered = false;
}
catch (OperationCanceledException)
{
Logger.LogInformation("取消挖矿");
}
catch (Exception e)
{
Logger.LogError("挖矿异常: {Msg}", e.Message);
}
finally
{
if (aimingModeEntered)
{
Simulation.SendInput.Keyboard.KeyPress(GIActions.SwitchAimingMode.ToActionKey().ToVK());
}
Simulation.SendInput.Mouse.MiddleButtonUp();
VisionContext.Instance().DrawContent.ClearAll();
}
}
private long _lastRefreshTime;
private async Task<(bool aligned, bool counted, int compensateDx, int compensateDy)> AlignAndMine(
MineralCluster cluster, double centerX, double centerY, CancellationToken ct)
{
var totalDx = 0;
var totalDy = 0;
var hadResult = true;
for (var retry = 0; retry < MaxInnerRetry && !ct.IsCancellationRequested; retry++)
{
if (Environment.TickCount64 - _lastRefreshTime >= ElementSightRefreshMs)
{
Simulation.SendInput.Mouse.MiddleButtonUp();
await Delay(100, ct);
Simulation.SendInput.Mouse.MiddleButtonDown();
await Delay(1500, ct);
_lastRefreshTime = Environment.TickCount64;
}
var offsetX = cluster.TargetX - centerX;
var offsetY = cluster.TargetY - centerY;
var isLast = retry == MaxInnerRetry - 1;
var isAligned = Math.Abs(offsetX) <= (cluster.TargetWidth + AlignmentExpansion * 2) / 2
&& Math.Abs(offsetY) <= (cluster.TargetHeight + AlignmentExpansion * 2) / 2;
// 前面所有循环都检测成功时,以不计入总次数的方式兜底射击一次
if (isAligned || (isLast && hadResult))
{
Simulation.SendInput.Mouse.MiddleButtonUp();
await Delay(300, ct);
Logger.LogInformation("开始挖矿");
await Mine(ct, totalDy < 0);
return (true, isAligned, 0, 0);
}
var mouseDx = (int)(offsetX * _dpi * AimSensitivityFactorX / _widthScale);
var mouseDy = (int)(offsetY * _dpi * AimSensitivityFactorY / _heightScale);
Simulation.SendInput.Mouse.MoveMouseBy(mouseDx, mouseDy);
totalDx += mouseDx;
totalDy += mouseDy;
await Delay(150, ct);
(cluster, centerX, centerY) = FindNearestMineralCluster();
if (cluster == null)
{
hadResult = false;
return (false, false, totalDx, totalDy);
}
}
return (false, false, totalDx, totalDy);
}
///
/// 执行挖矿操作
///
private static async Task Mine(CancellationToken ct, bool compensateUp)
{
if (compensateUp)
{
Simulation.SendInput.Mouse.MoveMouseBy(0, -25);
await Delay(10, ct);
}
Simulation.SendInput.Mouse.LeftButtonClick();
await Delay(2000, ct);
}
private void SaveDebugImage(Mat mat)
{
var debugDir = Path.Combine(Global.Absolute("log"), "DebugMine");
if (!Directory.Exists(debugDir))
{
Directory.CreateDirectory(debugDir);
}
else
{
_debugIndex = Directory.GetFiles(debugDir, "detect_*.png")
.Select(f => Path.GetFileNameWithoutExtension(f).Replace("detect_", ""))
.Where(n => int.TryParse(n, out _))
.Select(int.Parse)
.DefaultIfEmpty(-1)
.Max() + 1;
}
Cv2.ImWrite(Path.Combine(debugDir, $"detect_{_debugIndex++:D3}.png"), mat);
}
///
/// 检测矿物,返回距屏幕中心最近的聚堆
///
private (MineralCluster? cluster, double centerX, double centerY) FindNearestMineralCluster()
{
var systemInfo = TaskContext.Instance().SystemInfo;
var image = CaptureGameImage(TaskTriggerDispatcher.GlobalGameCapture);
var ra = systemInfo.DesktopRectArea.Derive(image, systemInfo.CaptureAreaRect.X, systemInfo.CaptureAreaRect.Y);
// SaveDebugImage(ra.SrcMat);
var rawResult = _predictor.Predictor.Detect(ra.CacheImage);
var centerX = ra.CacheImage.Width / 2.0;
var centerY = ra.CacheImage.Height / 2.0;
var oreBoxes = rawResult
.Where(box => box.Name.Name is "ore" && box.Confidence >= ConfidenceThreshold)
.Select(box => new Rect(
(int)box.Bounds.X,
(int)box.Bounds.Y,
(int)box.Bounds.Width,
(int)box.Bounds.Height))
.ToList();
// 画框
var drawList = oreBoxes.Select(r => ra.ToRectDrawable(r, "ore")).ToList();
VisionContext.Instance().DrawContent.PutOrRemoveRectList("BgiMine", drawList);
if (oreBoxes.Count == 0)
{
VisionContext.Instance().DrawContent.PutOrRemoveRectList("MiningCluster", null);
return (null, centerX, centerY);
}
var clusters = ClusterMinerals(oreBoxes);
// 画聚类目标矿物框
var expansion = (int)AlignmentExpansion;
var clusterDrawList = clusters.Select(c =>
{
var mark = new Rect((int)(c.TargetX - c.TargetWidth / 2) - expansion, (int)(c.TargetY - c.TargetHeight / 2) - expansion,
(int)c.TargetWidth + expansion * 2, (int)c.TargetHeight + expansion * 2);
return ra.ToRectDrawable(mark,
$"({(int)c.TargetX},{(int)c.TargetY})",
new Pen(Color.DodgerBlue, 2)
);
}).ToList();
VisionContext.Instance().DrawContent.PutOrRemoveRectList("MiningCluster", clusterDrawList);
// 忽略屏幕边缘聚类,仅当中间区域存在聚类时生效
var imgW = ra.CacheImage.Width;
var imgH = ra.CacheImage.Height;
var centerClusters = clusters
.Where(c => c.CenterX >= EdgeIgnore && c.CenterX <= imgW - EdgeIgnore
&& c.CenterY >= EdgeIgnore && c.CenterY <= imgH - EdgeIgnore)
.ToList();
var candidates = centerClusters.Count > 0 ? centerClusters : clusters;
var nearest = candidates
.OrderBy(c => Math.Pow(c.CenterX - centerX, 2) + Math.Pow(c.CenterY - centerY, 2))
.First();
return (nearest, centerX, centerY);
}
///
/// 贪心聚类:距离小于阈值的检测框归入同一簇,阈值根据元素面积动态缩放
///
private List ClusterMinerals(List rects)
{
if (rects.Count == 0) return [];
var refArea = BaseClusterArea * _widthScale * _widthScale;
var clusters = new List();
foreach (var rect in rects)
{
var cx = rect.X + rect.Width / 2.0;
var cy = rect.Y + rect.Height / 2.0;
MineralCluster? nearest = null;
double nearestDist = double.MaxValue;
foreach (var cluster in clusters)
{
var dist = Math.Sqrt(Math.Pow(cx - cluster.CenterX, 2) + Math.Pow(cy - cluster.CenterY, 2));
if (dist < nearestDist)
{
nearestDist = dist;
nearest = cluster;
}
}
if (nearest != null)
{
var clusterAvgArea = nearest.Rects.Average(r => (double)r.Width * r.Height);
var rectArea = (double)rect.Width * rect.Height;
var combinedAvg = (clusterAvgArea * nearest.Rects.Count + rectArea) / (nearest.Rects.Count + 1);
var effectiveThreshold = ClusterDistanceThreshold * Math.Sqrt(combinedAvg / Math.Max(1, refArea));
if (nearestDist < effectiveThreshold && nearest.TryAddRect(rect))
{
continue;
}
}
clusters.Add(new MineralCluster(rect, AreaRatioThreshold, _preferRight));
}
return clusters;
}
}
///
/// 矿物聚堆,维护一组相近矿物的检测框及质心
///
public class MineralCluster
{
public List Rects { get; } = new();
public double CenterX { get; private set; }
public double CenterY { get; private set; }
public double TargetX { get; private set; }
public double TargetY { get; private set; }
public double TargetWidth { get; private set; }
public double TargetHeight { get; private set; }
public MineralCluster(Rect firstRect, double areaRatioThreshold = 5, bool preferRight = true)
{
AreaRatioThreshold = areaRatioThreshold;
PreferRight = preferRight;
Rects.Add(firstRect);
RecalculateCenter();
}
private readonly double AreaRatioThreshold;
private readonly bool PreferRight;
public bool TryAddRect(Rect rect)
{
var avgArea = Rects.Average(r => (double)r.Width * r.Height);
var newArea = (double)rect.Width * rect.Height;
if (newArea > avgArea * AreaRatioThreshold || newArea < avgArea / AreaRatioThreshold) return false;
Rects.Add(rect);
RecalculateCenter();
return true;
}
private void RecalculateCenter()
{
CenterX = Rects.Average(r => r.X + r.Width / 2.0);
CenterY = Rects.Average(r => r.Y + r.Height / 2.0);
var sorted = Rects
.Select(r => (cx: r.X + r.Width / 2.0, cy: r.Y + r.Height / 2.0,
dist: Math.Pow(r.X + r.Width / 2.0 - CenterX, 2) + Math.Pow(r.Y + r.Height / 2.0 - CenterY, 2),
w: (double)r.Width, h: (double)r.Height))
.OrderBy(t => t.dist)
.ToList();
(double cx, double cy, double dist, double w, double h) target;
if (PreferRight && sorted.Count >= 2)
{
// 多轮旋转时:取最近2个中靠右的,对冲左转偏移
target = sorted.Take(2).OrderByDescending(t => t.cx).First();
}
else
{
// 单轮时:直接选最近的
target = sorted.First();
}
TargetX = target.cx;
TargetY = target.cy;
TargetWidth = target.w;
TargetHeight = target.h;
}
}