diff --git a/BetterGenshinImpact/BetterGenshinImpact.csproj b/BetterGenshinImpact/BetterGenshinImpact.csproj index 618fc02f..2647a869 100644 --- a/BetterGenshinImpact/BetterGenshinImpact.csproj +++ b/BetterGenshinImpact/BetterGenshinImpact.csproj @@ -71,6 +71,108 @@ Always + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + Always diff --git a/BetterGenshinImpact/Core/Recognition/OpenCv/CommonExtension.cs b/BetterGenshinImpact/Core/Recognition/OpenCv/CommonExtension.cs index 5645efc7..abc5e1f3 100644 --- a/BetterGenshinImpact/Core/Recognition/OpenCv/CommonExtension.cs +++ b/BetterGenshinImpact/Core/Recognition/OpenCv/CommonExtension.cs @@ -4,6 +4,9 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using System.Drawing; +using OpenCvSharp; +using Vanara.PInvoke; +using Point = System.Drawing.Point; namespace BetterGenshinImpact.Core.Recognition.OpenCv { @@ -55,6 +58,27 @@ namespace BetterGenshinImpact.Core.Recognition.OpenCv } + public static OpenCvSharp.Point GetCenterPoint(this RECT rectangle) + { + if (rectangle.IsEmpty) + { + throw new ArgumentException("rectangle is empty"); + } + + return new OpenCvSharp.Point(rectangle.X + rectangle.Width / 2, rectangle.Y + rectangle.Height / 2); + } + + public static OpenCvSharp.Point GetCenterPoint(this Rect rectangle) + { + if (rectangle == Rect.Empty) + { + throw new ArgumentException("rectangle is empty"); + } + + return new OpenCvSharp.Point(rectangle.X + rectangle.Width / 2, rectangle.Y + rectangle.Height / 2); + } + + public static System.Windows.Media.Color ToWindowsColor(this Color color) { return System.Windows.Media.Color.FromArgb(color.A, color.R, color.G, color.B); diff --git a/BetterGenshinImpact/Core/Recognition/OpenCv/MatchTemplateHelper.cs b/BetterGenshinImpact/Core/Recognition/OpenCv/MatchTemplateHelper.cs index ce221497..8dfe81ff 100644 --- a/BetterGenshinImpact/Core/Recognition/OpenCv/MatchTemplateHelper.cs +++ b/BetterGenshinImpact/Core/Recognition/OpenCv/MatchTemplateHelper.cs @@ -1,13 +1,8 @@ -using OpenCvSharp; +using Microsoft.Extensions.Logging; +using OpenCvSharp; using System; using System.Collections.Generic; -using System.Drawing; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; using Point = OpenCvSharp.Point; -using BetterGenshinImpact.GameTask.AutoSkip; namespace BetterGenshinImpact.Core.Recognition.OpenCv { @@ -17,12 +12,13 @@ namespace BetterGenshinImpact.Core.Recognition.OpenCv /// /// 模板匹配 + /// TODO 算法不一样的的时候找点的方法也不一样 /// - /// - /// - /// - /// - /// + /// 原图像 + /// 模板 + /// 匹配方式 + /// 遮罩 + /// 阈值 /// 左上角的标点 public static Point MatchTemplate(Mat srcMat, Mat dstMat, TemplateMatchModes matchMode, Mat? maskMat = null, double threshold = 0.8) { @@ -53,5 +49,60 @@ namespace BetterGenshinImpact.Core.Recognition.OpenCv return new Point(); } } + + /// + /// 模板匹配多个结果 + /// + /// + /// + /// + /// + /// + /// + public static List MatchTemplateMulti(Mat srcMat, Mat dstMat, TemplateMatchModes matchMode = TemplateMatchModes.CCoeffNormed, Mat? maskMat = null, double threshold = 0.8) + { + var points = new List(); + try + { + using var result = new Mat(); + if (maskMat == null) + { + Cv2.MatchTemplate(srcMat, dstMat, result, matchMode); + } + else + { + Cv2.MatchTemplate(srcMat, dstMat, result, matchMode, maskMat); + } + + while (true) + { + Cv2.MinMaxLoc(result, out _, out var maxValue, out _, out var maxLoc); + + if (maxValue >= threshold) + { + points.Add(new Point(maxLoc.X, maxLoc.Y)); + + //Fill in the res Mat so you don't find the same area again in the MinMaxLoc + Cv2.FloodFill(result, maxLoc, new Scalar(0), out _, new Scalar(0.1), new Scalar(1.0)); + } + else + { + break; + } + } + + return points; + } + catch (Exception ex) + { + _logger.LogError("{Ex}", ex); + return points; + } + } + + public static List MatchTemplateMulti(Mat srcMat, Mat dstMat, double threshold) + { + return MatchTemplateMulti(srcMat, dstMat, TemplateMatchModes.CCoeffNormed, null, threshold); + } } } \ No newline at end of file diff --git a/BetterGenshinImpact/Core/Recognition/OpenCv/OldMatchTemplateHelper.cs b/BetterGenshinImpact/Core/Recognition/OpenCv/OldMatchTemplateHelper.cs index a608410b..02b4ec5f 100644 --- a/BetterGenshinImpact/Core/Recognition/OpenCv/OldMatchTemplateHelper.cs +++ b/BetterGenshinImpact/Core/Recognition/OpenCv/OldMatchTemplateHelper.cs @@ -6,177 +6,177 @@ using System.Collections.Generic; using System.Drawing; using Point = OpenCvSharp.Point; -namespace BetterGenshinImpact.Core.Recognition.OpenCv +namespace BetterGenshinImpact.Core.Recognition.OpenCv; + +[Obsolete] +public class OldMatchTemplateHelper { - public class OldMatchTemplateHelper - { - private static readonly ILogger _logger = App.GetLogger(); + private static readonly ILogger _logger = App.GetLogger(); - public static double WidthScale = 1; - public static double HeightScale = 1; + public static double WidthScale = 1; + public static double HeightScale = 1; - public static Point FindSingleTarget(Bitmap imgSrc, Bitmap imgSub, double threshold = 0.8) - { - Mat? srcMat = null; - Mat? dstMat = null; - try - { - srcMat = imgSrc.ToMat(); - dstMat = imgSub.ToMat(); - return FindSingleTarget(srcMat, dstMat, threshold); - } - catch (Exception ex) - { - _logger.LogError(ex.ToString()); - return new Point(); - } - finally - { - srcMat?.Dispose(); - dstMat?.Dispose(); - } - } + //public static Point FindSingleTarget(Bitmap imgSrc, Bitmap imgSub, double threshold = 0.8) + //{ + // Mat? srcMat = null; + // Mat? dstMat = null; + // try + // { + // srcMat = imgSrc.ToMat(); + // dstMat = imgSub.ToMat(); + // return FindSingleTarget(srcMat, dstMat, threshold); + // } + // catch (Exception ex) + // { + // _logger.LogError(ex.ToString()); + // return new Point(); + // } + // finally + // { + // srcMat?.Dispose(); + // dstMat?.Dispose(); + // } + //} - public static Point FindSingleTarget(Mat srcMat, Mat dstMat, double threshold = 0.8) - { - Point p = new Point(); + //public static Point FindSingleTarget(Mat srcMat, Mat dstMat, double threshold = 0.8) + //{ + // Point p = new Point(); - OutputArray? outArray = null; - try - { - dstMat = ResizeHelper.Resize(dstMat, WidthScale); + // OutputArray? outArray = null; + // try + // { + // dstMat = ResizeHelper.Resize(dstMat, WidthScale); - outArray = OutputArray.Create(srcMat); - Cv2.MatchTemplate(srcMat, dstMat, outArray, TemplateMatchModes.CCoeffNormed); - double minValue, maxValue; - Point location, point; - Cv2.MinMaxLoc(InputArray.Create(outArray.GetMat()), out minValue, out maxValue, - out location, out point); + // outArray = OutputArray.Create(srcMat); + // Cv2.MatchTemplate(srcMat, dstMat, outArray, TemplateMatchModes.CCoeffNormed); + // double minValue, maxValue; + // Point location, point; + // Cv2.MinMaxLoc(InputArray.Create(outArray.GetMat()), out minValue, out maxValue, + // out location, out point); - if (maxValue >= threshold) - { - p = new Point(point.X + dstMat.Width / 2, point.Y + dstMat.Height / 2); - //if (VisionContext.Instance().Drawable) - //{ - //VisionContext.Instance().DrawContent.PutRect("", new System.Windows.Rect(point.X, point.Y, dstMat.Width, dstMat.Height)); - //VisionContext.Instance().DrawContent.TextList - // .Add(new Tuple(new System.Windows.Point(point.X, point.Y - 10), maxValue.ToString("0.00"))); - //} - } + // if (maxValue >= threshold) + // { + // p = new Point(point.X + dstMat.Width / 2, point.Y + dstMat.Height / 2); + // if (VisionContext.Instance().Drawable) + // { + // VisionContext.Instance().DrawContent.PutRect("", new System.Windows.Rect(point.X, point.Y, dstMat.Width, dstMat.Height)); + // VisionContext.Instance().DrawContent.TextList + // .Add(new Tuple(new System.Windows.Point(point.X, point.Y - 10), maxValue.ToString("0.00"))); + // } + // } - return p; - } - catch (Exception ex) - { - _logger.LogError(ex.ToString()); - return p; - } - finally - { - outArray?.Dispose(); - } - } + // return p; + // } + // catch (Exception ex) + // { + // _logger.LogError(ex.ToString()); + // return p; + // } + // finally + // { + // outArray?.Dispose(); + // } + //} - public static List FindMultiTarget(Mat srcMat, Mat dstMat, string title, out Mat resMat, - double threshold = 0.8, int findTargetCount = 8) - { - List pointList = new List(); - resMat = srcMat.Clone(); - try - { - dstMat = ResizeHelper.Resize(dstMat, WidthScale); + //public static List FindMultiTarget(Mat srcMat, Mat dstMat, string title, out Mat resMat, + // double threshold = 0.8, int findTargetCount = 8) + //{ + // List pointList = new List(); + // resMat = srcMat.Clone(); + // try + // { + // dstMat = ResizeHelper.Resize(dstMat, WidthScale); - Mat matchResult = new Mat(); - Cv2.MatchTemplate(srcMat, dstMat, matchResult, TemplateMatchModes.CCoeffNormed); + // Mat matchResult = new Mat(); + // Cv2.MatchTemplate(srcMat, dstMat, matchResult, TemplateMatchModes.CCoeffNormed); - double minValue = 0; - double maxValue = 0; - Point minLoc = new(); + // double minValue = 0; + // double maxValue = 0; + // Point minLoc = new(); - //寻找最几个最值的位置 - Mat mask = new Mat(matchResult.Height, matchResult.Width, MatType.CV_8UC1, Scalar.White); - Mat maskSub = new Mat(matchResult.Height, matchResult.Width, MatType.CV_8UC1, Scalar.Black); - var point = new OpenCvSharp.Point(0, 0); - for (int i = 0; i < findTargetCount; i++) - { - Cv2.MinMaxLoc(matchResult, out minValue, out maxValue, out minLoc, out point, mask); - Rect maskRect = new Rect(point.X - dstMat.Width / 2, point.Y - dstMat.Height / 2, dstMat.Width, - dstMat.Height); - maskSub.Rectangle(maskRect, Scalar.White, -1); - mask -= maskSub; - if (maxValue >= threshold) - { - pointList.Add(new Point(point.X + dstMat.Width / 2, point.Y + dstMat.Height / 2)); + // //寻找最几个最值的位置 + // Mat mask = new Mat(matchResult.Height, matchResult.Width, MatType.CV_8UC1, Scalar.White); + // Mat maskSub = new Mat(matchResult.Height, matchResult.Width, MatType.CV_8UC1, Scalar.Black); + // var point = new OpenCvSharp.Point(0, 0); + // for (int i = 0; i < findTargetCount; i++) + // { + // Cv2.MinMaxLoc(matchResult, out minValue, out maxValue, out minLoc, out point, mask); + // Rect maskRect = new Rect(point.X - dstMat.Width / 2, point.Y - dstMat.Height / 2, dstMat.Width, + // dstMat.Height); + // maskSub.Rectangle(maskRect, Scalar.White, -1); + // mask -= maskSub; + // if (maxValue >= threshold) + // { + // pointList.Add(new Point(point.X + dstMat.Width / 2, point.Y + dstMat.Height / 2)); - //if (VisionContext.Instance().Drawable) - //{ - // VisionContext.Instance().DrawContent.RectList - // .Add(new System.Windows.Rect(point.X, point.Y, dstMat.Width, dstMat.Height)); - // VisionContext.Instance().DrawContent.TextList - // .Add(new Tuple(new System.Windows.Point(point.X, point.Y - 10), maxValue.ToString("0.00"))); - //} - //if (IsDebug) - //{ - // VisionContext.Instance().Log - // ?.LogInformation(title + " " + maxValue.ToString("0.000") + " " + point); - // Cv2.Rectangle(resMat, point, - // new OpenCvSharp.Point(point.X + dstMat.Width, point.Y + dstMat.Height), - // Scalar.Red, 2); - // Cv2.PutText(resMat, title + " " + maxValue.ToString("0.00"), - // new OpenCvSharp.Point(point.X, point.Y - 10), - // HersheyFonts.HersheySimplex, 0.5, Scalar.Red); - //} - } - else - { - break; - } - } + // //if (VisionContext.Instance().Drawable) + // //{ + // // VisionContext.Instance().DrawContent.RectList + // // .Add(new System.Windows.Rect(point.X, point.Y, dstMat.Width, dstMat.Height)); + // // VisionContext.Instance().DrawContent.TextList + // // .Add(new Tuple(new System.Windows.Point(point.X, point.Y - 10), maxValue.ToString("0.00"))); + // //} + // //if (IsDebug) + // //{ + // // VisionContext.Instance().Log + // // ?.LogInformation(title + " " + maxValue.ToString("0.000") + " " + point); + // // Cv2.Rectangle(resMat, point, + // // new OpenCvSharp.Point(point.X + dstMat.Width, point.Y + dstMat.Height), + // // Scalar.Red, 2); + // // Cv2.PutText(resMat, title + " " + maxValue.ToString("0.00"), + // // new OpenCvSharp.Point(point.X, point.Y - 10), + // // HersheyFonts.HersheySimplex, 0.5, Scalar.Red); + // //} + // } + // else + // { + // break; + // } + // } - return pointList; - } - catch (Exception ex) - { - _logger.LogError(ex.ToString()); - return pointList; - } - finally - { - srcMat?.Dispose(); - dstMat?.Dispose(); - } - } + // return pointList; + // } + // catch (Exception ex) + // { + // _logger.LogError(ex.ToString()); + // return pointList; + // } + // finally + // { + // srcMat?.Dispose(); + // dstMat?.Dispose(); + // } + //} - public static Dictionary> FindMultiPicFromOneImage(Bitmap imgSrc, - Dictionary imgSubDictionary, double threshold = 0.8) - { - Dictionary> dictionary = new Dictionary>(); - Mat srcMat = imgSrc.ToMat(); - Mat resMat; + //public static Dictionary> FindMultiPicFromOneImage(Bitmap imgSrc, + // Dictionary imgSubDictionary, double threshold = 0.8) + //{ + // Dictionary> dictionary = new Dictionary>(); + // Mat srcMat = imgSrc.ToMat(); + // Mat resMat; - foreach (KeyValuePair kvp in imgSubDictionary) - { - dictionary.Add(kvp.Key, FindMultiTarget(srcMat, kvp.Value.ToMat(), kvp.Key, out resMat, threshold)); - srcMat = resMat.Clone(); - } + // foreach (KeyValuePair kvp in imgSubDictionary) + // { + // dictionary.Add(kvp.Key, FindMultiTarget(srcMat, kvp.Value.ToMat(), kvp.Key, out resMat, threshold)); + // srcMat = resMat.Clone(); + // } - return dictionary; - } + // return dictionary; + //} - public static Dictionary> FindMultiPicFromOneImage(Mat srcMat, - Dictionary imgSubDictionary, double threshold = 0.8) - { - Dictionary> dictionary = new Dictionary>(); - Mat resMat; - foreach (KeyValuePair kvp in imgSubDictionary) - { - dictionary.Add(kvp.Key, FindMultiTarget(srcMat, kvp.Value.ToMat(), kvp.Key, out resMat, threshold)); - srcMat = resMat.Clone(); - } + //public static Dictionary> FindMultiPicFromOneImage(Mat srcMat, + // Dictionary imgSubDictionary, double threshold = 0.8) + //{ + // Dictionary> dictionary = new Dictionary>(); + // Mat resMat; + // foreach (KeyValuePair kvp in imgSubDictionary) + // { + // dictionary.Add(kvp.Key, FindMultiTarget(srcMat, kvp.Value.ToMat(), kvp.Key, out resMat, threshold)); + // srcMat = resMat.Clone(); + // } - return dictionary; - } - } + // return dictionary; + //} } \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_anemo.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_anemo.png new file mode 100644 index 00000000..b0e95226 Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_anemo.png differ diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_cryo.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_cryo.png new file mode 100644 index 00000000..0373df92 Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_cryo.png differ diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_dendro.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_dendro.png new file mode 100644 index 00000000..ffe18b2d Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_dendro.png differ diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_electro.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_electro.png new file mode 100644 index 00000000..a5fcb6d9 Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_electro.png differ diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_geo.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_geo.png new file mode 100644 index 00000000..996c6c01 Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_geo.png differ diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_hydro.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_hydro.png new file mode 100644 index 00000000..b44962df Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_hydro.png differ diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_omni.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_omni.png new file mode 100644 index 00000000..e735dabb Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_omni.png differ diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_pyro.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_pyro.png new file mode 100644 index 00000000..e558810a Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/action_pyro.png differ diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_anemo.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_anemo.png new file mode 100644 index 00000000..b7325d0f Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_anemo.png differ diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_cryo.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_cryo.png new file mode 100644 index 00000000..92ef3ec9 Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_cryo.png differ diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_dendro.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_dendro.png new file mode 100644 index 00000000..3a1a82b5 Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_dendro.png differ diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_electro.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_electro.png new file mode 100644 index 00000000..c6ca2d57 Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_electro.png differ diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_geo.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_geo.png new file mode 100644 index 00000000..52de0a99 Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_geo.png differ diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_hydro.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_hydro.png new file mode 100644 index 00000000..6c5d0641 Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_hydro.png differ diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_omni.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_omni.png new file mode 100644 index 00000000..9c17eac0 Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_omni.png differ diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_pyro.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_pyro.png new file mode 100644 index 00000000..3c3a563f Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/dice/roll_pyro.png differ diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/元素调和.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/元素调和.png new file mode 100644 index 00000000..d94d2869 Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/元素调和.png differ diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/元素骰子不足.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/元素骰子不足.png new file mode 100644 index 00000000..763160b8 Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/元素骰子不足.png differ diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/冻结.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/冻结.png new file mode 100644 index 00000000..00e0e67e Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/冻结.png differ diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/出战角色.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/出战角色.png new file mode 100644 index 00000000..fc49ffe0 Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/出战角色.png differ diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/回合结束.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/回合结束.png new file mode 100644 index 00000000..1017cbd9 Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/回合结束.png differ diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/回合结算阶段.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/回合结算阶段.png new file mode 100644 index 00000000..707c8201 Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/回合结算阶段.png differ diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/对方行动中.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/对方行动中.png new file mode 100644 index 00000000..256b1b4f Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/对方行动中.png differ diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/满能量.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/满能量.png new file mode 100644 index 00000000..b438849f Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/满能量.png differ diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/确定.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/确定.png new file mode 100644 index 00000000..37c12d1d Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/确定.png differ diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/确定_1600x900.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/确定_1600x900.png new file mode 100644 index 00000000..cfa6128d Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/确定_1600x900.png differ diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/空能量.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/空能量.png new file mode 100644 index 00000000..1657ba7c Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/空能量.png differ diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/角色死亡.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/角色死亡.png new file mode 100644 index 00000000..7ddb50fa Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/角色死亡.png differ diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/角色状态_冻结.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/角色状态_冻结.png new file mode 100644 index 00000000..aa5172dd Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/角色状态_冻结.png differ diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/角色状态_冻结2.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/角色状态_冻结2.png new file mode 100644 index 00000000..00e0e67e Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/角色状态_冻结2.png differ diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/角色状态_水泡.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/角色状态_水泡.png new file mode 100644 index 00000000..bfaed8ac Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/角色状态_水泡.png differ diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/角色血量上方.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/角色血量上方.png new file mode 100644 index 00000000..92873cfa Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/角色血量上方.png differ diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/角色被打败.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/角色被打败.png new file mode 100644 index 00000000..a967378c Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/角色被打败.png differ diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/退出挑战.png b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/退出挑战.png new file mode 100644 index 00000000..b049f962 Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/1920x1080/other/退出挑战.png differ diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/AutoGeniusInvokationAssets.cs b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/AutoGeniusInvokationAssets.cs new file mode 100644 index 00000000..30676c5b --- /dev/null +++ b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/AutoGeniusInvokationAssets.cs @@ -0,0 +1,154 @@ +using BetterGenshinImpact.Core.Config; +using OpenCvSharp; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BetterGenshinImpact.Core.Recognition; + +namespace BetterGenshinImpact.GameTask.AutoGeniusInvokation.Assets +{ + public class AutoGeniusInvokationAssets + { + public RecognitionObject ConfirmButtonRo; + public RecognitionObject RoundEndButtonRo; + public RecognitionObject ElementalTuningConfirmButtonRo; + public RecognitionObject ExitDuelButtonRo; + + public RecognitionObject InOpponentActionRo; + public RecognitionObject EndPhaseRo; + public RecognitionObject ElementalDiceLackWarningRo; + public RecognitionObject CharacterTakenOutRo; + public Mat CharacterDefeatedMat; + public RecognitionObject InCharacterPickRo; + + // 角色区域 + public RecognitionObject CharacterHpUpperRo; + public Mat CharacterStatusFreezeMat; + public Mat CharacterStatusDizzinessMat; + public Mat CharacterEnergyOnMat; + + public Dictionary RollPhaseDiceMats; + public Dictionary ActionPhaseDiceMats; + + public AutoGeniusInvokationAssets() + { + var info = TaskContext.Instance().SystemInfo; + ConfirmButtonRo = new RecognitionObject + { + Name = "ConfirmButton", + RecognitionType = RecognitionTypes.TemplateMatch, + TemplateImageMat = GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"other\确定.png"), + DrawOnWindow = true + }.InitTemplate(); + RoundEndButtonRo = new RecognitionObject + { + Name = "RoundEndButton", + RecognitionType = RecognitionTypes.TemplateMatch, + TemplateImageMat = GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"other\回合结束.png"), + RegionOfInterest = new Rect(0, 0, info.CaptureAreaRect.Width / 5, info.CaptureAreaRect.Height), + DrawOnWindow = true + }.InitTemplate(); + ElementalTuningConfirmButtonRo = new RecognitionObject + { + Name = "ElementalTuningConfirmButton", + RecognitionType = RecognitionTypes.TemplateMatch, + TemplateImageMat = GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"other\元素调和.png"), + DrawOnWindow = true + }.InitTemplate(); + ExitDuelButtonRo = new RecognitionObject + { + Name = "ExitDuelButton", + RecognitionType = RecognitionTypes.TemplateMatch, + TemplateImageMat = GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"other\退出挑战.png"), + RegionOfInterest = new Rect(0, info.CaptureAreaRect.Height / 2, info.CaptureAreaRect.Width / 2, info.CaptureAreaRect.Height - info.CaptureAreaRect.Height / 2), + DrawOnWindow = true + }.InitTemplate(); + InOpponentActionRo = new RecognitionObject + { + Name = "InOpponentAction", + RecognitionType = RecognitionTypes.TemplateMatch, + TemplateImageMat = GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"other\对方行动中.png"), + RegionOfInterest = new Rect(0, 0, info.CaptureAreaRect.Width / 5, info.CaptureAreaRect.Height), + DrawOnWindow = true + }.InitTemplate(); + EndPhaseRo = new RecognitionObject + { + Name = "EndPhase", + RecognitionType = RecognitionTypes.TemplateMatch, + TemplateImageMat = GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"other\回合结算阶段.png"), + RegionOfInterest = new Rect(0, 0, info.CaptureAreaRect.Width / 5, info.CaptureAreaRect.Height), + DrawOnWindow = true + }.InitTemplate(); + ElementalDiceLackWarningRo = new RecognitionObject + { + Name = "ElementalDiceLackWarning", + RecognitionType = RecognitionTypes.TemplateMatch, + TemplateImageMat = GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"other\元素骰子不足.png"), + RegionOfInterest = new Rect(info.CaptureAreaRect.Width - info.CaptureAreaRect.Width / 2, 0, + info.CaptureAreaRect.Width / 2, info.CaptureAreaRect.Height), + DrawOnWindow = true + }.InitTemplate(); + CharacterTakenOutRo = new RecognitionObject + { + Name = "CharacterTakenOut", + RecognitionType = RecognitionTypes.TemplateMatch, + TemplateImageMat = GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"other\角色死亡.png"), + DrawOnWindow = true + }.InitTemplate(); + + + CharacterDefeatedMat = GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"other\角色被打败.png"); + + InCharacterPickRo = new RecognitionObject + { + Name = "InCharacterPick", + RecognitionType = RecognitionTypes.TemplateMatch, + TemplateImageMat = GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"other\出战角色.png"), + RegionOfInterest = new Rect(info.CaptureAreaRect.Width / 2, info.CaptureAreaRect.Height / 2, + info.CaptureAreaRect.Width - info.CaptureAreaRect.Width / 2, + info.CaptureAreaRect.Height - info.CaptureAreaRect.Height / 2), + DrawOnWindow = true + }.InitTemplate(); + CharacterHpUpperRo = new RecognitionObject + { + Name = "CharacterHpUpper", + RecognitionType = RecognitionTypes.TemplateMatch, + TemplateImageMat = GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"other\角色血量上方.png"), + DrawOnWindow = true + }.InitTemplate(); + + + CharacterStatusFreezeMat = GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"other\角色状态_冻结.png"); + CharacterStatusDizzinessMat = GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"other\角色状态_水泡.png"); + CharacterEnergyOnMat = GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"other\满能量.png"); + + // 投掷期间的骰子 + RollPhaseDiceMats = new Dictionary() + { + { "anemo", GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"dice\roll_anemo.png") }, + { "geo", GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"dice\roll_geo.png.png") }, + { "electro", GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"dice\roll_electro.png.png") }, + { "dendro", GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"dice\roll_dendro.png.png") }, + { "hydro", GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"dice\roll_hydro.png.png") }, + { "pyro", GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"dice\roll_pyro.png.png") }, + { "cryo", GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"dice\roll_cryo.png.png") }, + { "omni", GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"dice\roll_omni.png.png") }, + }; + + // 主界面骰子 + ActionPhaseDiceMats = new Dictionary() + { + { "anemo", GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"dice\action_anemo.png.png") }, + { "geo", GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"dice\action_geo.png.png") }, + { "electro", GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"dice\action_electro.png.png") }, + { "dendro", GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"dice\action_dendro.png.png") }, + { "hydro", GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"dice\action_hydro.png.png") }, + { "pyro", GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"dice\action_pyro.png.png") }, + { "cryo", GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"dice\action_cryo.png.png") }, + { "omni", GameTaskManager.LoadAssertImage("AutoGeniusInvokation", @"dice\action_omni.png.png") }, + }; + } + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Exception/DuelEndException.cs b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Exception/DuelEndException.cs new file mode 100644 index 00000000..67357681 --- /dev/null +++ b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Exception/DuelEndException.cs @@ -0,0 +1,8 @@ +namespace BetterGenshinImpact.GameTask.AutoGeniusInvokation.Exception; + +public class DuelEndException: System.Exception +{ + public DuelEndException(string message) : base(message) + { + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Exception/RetryException.cs b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Exception/RetryException.cs new file mode 100644 index 00000000..79435647 --- /dev/null +++ b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Exception/RetryException.cs @@ -0,0 +1,12 @@ +namespace BetterGenshinImpact.GameTask.AutoGeniusInvokation.Exception; + +public class RetryException : System.Exception +{ + public RetryException() : base() + { + } + + public RetryException(string message) : base(message) + { + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/GeniusInvokationControl.cs b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/GeniusInvokationControl.cs new file mode 100644 index 00000000..5b9a5296 --- /dev/null +++ b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/GeniusInvokationControl.cs @@ -0,0 +1,1271 @@ +using BetterGenshinImpact.Core.Recognition.OpenCv; +using BetterGenshinImpact.GameTask.AutoGeniusInvokation.Assets; +using BetterGenshinImpact.GameTask.AutoGeniusInvokation.Exception; +using BetterGenshinImpact.GameTask.AutoGeniusInvokation.Model; +using BetterGenshinImpact.GameTask.Model; +using BetterGenshinImpact.Helpers.Extensions; +using Fischless.GameCapture; +using GeniusInvokationAutoToy.Utils; +using Microsoft.Extensions.Logging; +using OpenCvSharp; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Point = OpenCvSharp.Point; + +namespace BetterGenshinImpact.GameTask.AutoGeniusInvokation; + +/// +/// 用于操控游戏 +/// +public class GeniusInvokationControl +{ + private readonly ILogger _logger = App.GetLogger(); + + // 定义一个静态变量来保存类的实例 + private static GeniusInvokationControl? _uniqueInstance; + + // 定义一个标识确保线程同步 + private static readonly object _locker = new object(); + + // 定义私有构造函数,使外界不能创建该类实例 + private GeniusInvokationControl() + { + } + + /// + /// 定义公有方法提供一个全局访问点,同时你也可以定义公有属性来提供全局访问点 + /// + /// + public static GeniusInvokationControl GetInstance() + { + if (_uniqueInstance == null) + { + lock (_locker) + { + _uniqueInstance ??= new GeniusInvokationControl(); + } + } + + return _uniqueInstance; + } + + public static bool OutputImageWhenError = true; + + + public CancellationTokenSource? cts; + + //public ImageCapture capture = new ImageCapture(); + //public YuanShenWindow window = new YuanShenWindow(); + //public Rectangle windowRect; + + private AutoGeniusInvokationAssets _assets = new(); + + private IGameCapture? _capture; + + public void Init(CancellationTokenSource cts1) + { + cts = cts1; + //if (!window.FindYSHandle()) + //{ + // throw new Exception("未找到原神进程,请先启动原神!"); + //} + } + + //public void FocusGameWindow() + //{ + // //window.Focus(); + //} + + //public string GetActiveProcessName() + //{ + // try + // { + // IntPtr hwnd = Native.GetForegroundWindow(); + // uint pid; + // GetWindowThreadProcessId(hwnd, out pid); + // Process p = Process.GetProcessById((int)pid); + // return p.ProcessName; + // } + // catch + // { + // return null; + // } + //} + + //public void IsGameFocus() + //{ + // windowRect = GetWindowRealRect(window); + // capture.Start(windowRect.X, windowRect.Y, windowRect.Width, windowRect.Height); + // if (windowRect.Width < 800) + // { + // throw new RetryException("原神窗口宽度小于800,请确认游戏已经获取到焦点!"); + // } + //} + + + // public Rectangle GetWindowRealRect(YuanShenWindow yuanShenWindow) + // { + // Rectangle rc = yuanShenWindow.GetSize(); + // int x = (int)Math.Ceiling(rc.Location.X * PrimaryScreen.ScaleX); + // int y = (int)Math.Ceiling(rc.Location.Y * PrimaryScreen.ScaleY); + // int w = (int)Math.Ceiling(rc.Width * PrimaryScreen.ScaleX); + // int h = (int)Math.Ceiling(rc.Height * PrimaryScreen.ScaleY); + // MyLogger.Debug($"原神窗口大小:{rc.Width} x {rc.Height}"); + // MyLogger.Debug($"原神窗口大小(计算DPI缩放后):{rc.Width * PrimaryScreen.ScaleX} x {rc.Height * PrimaryScreen.ScaleY}"); + // + // System.Drawing.Size size = new System.Drawing.Size(1920, 1080); + // if (w >= 1920 && w < 2000 && h >= 1080 && h < 1150) + // { + // size = new System.Drawing.Size(1920, 1080); + // ImageRecognition.WidthScale = 1; + // //ImageRecognition.HeightScale = 1; + // } + // else if (w >= 1600 && w < 2000 && h >= 900 && h < 950) + // { + // size = new System.Drawing.Size(1600, 900); + // ImageRecognition.WidthScale = 1600 * 1.0 / 1920; + // //ImageRecognition.HeightScale = 900 * 1.0 / 1080; + // } + // else + // { + // ImageRecognition.WidthScale = w * 1.0 / 1929; // 1929是计算了边缘 + // //ImageRecognition.HeightScale = h * 1.0 / 1109; // 1109是计算了窗口标题栏 + // } + // + // MyLogger.Debug($"匹配图片缩放比率:{ImageRecognition.WidthScale}"); + // return new Rectangle(x, y, w, h); + // } + + public void Sleep(int millisecondsTimeout) + { + CheckTask(); + Thread.Sleep(millisecondsTimeout); + } + + public Mat Capture() + { + CheckTask(); + //return capture.Capture(); + return null; + } + + public RectArea CaptureRectArea() + { + CheckTask(); + var bitmap = _capture.Capture(); + if (bitmap == null) + { + _logger.LogWarning("截图失败!"); + throw new RetryException("截图失败"); + } + + var systemInfo = TaskContext.Instance().SystemInfo; + return new RectArea(bitmap, systemInfo.CaptureAreaRect.X, systemInfo.CaptureAreaRect.Y, systemInfo.DesktopRectArea); + ; + } + + public void CheckTask() + { + Retry.Do(() => + { + if (cts != null && cts.IsCancellationRequested) + { + return; + } + + string name = "GetActiveProcessName()"; + if (!string.IsNullOrEmpty(name) && name != "YuanShen" && name != "GenshinImpact") + { + _logger.LogWarning("当前获取焦点的窗口进程名:{},不是原神窗口,暂停", name); + throw new RetryException("当前获取焦点的窗口不是原神窗口"); + } + }, TimeSpan.FromSeconds(1), 100); + + + if (cts != null && cts.IsCancellationRequested) + { + throw new TaskCanceledException("任务取消"); + } + } + + public void CommonDuelPrepare() + { + // 1. 选择初始手牌 + Sleep(1000); + _logger.LogInformation("开始选择初始手牌"); + while (!ClickConfirm()) + { + // 循环等待选择卡牌画面 + Sleep(1000); + } + + _logger.LogInformation("点击确认"); + + // 2. 选择出战角色 + // 此处选择第2个角色 雷神 + _logger.LogInformation("等待3s对局准备..."); + Sleep(3000); + + // 是否是再角色出战选择界面 + Retry.Do(IsInCharacterPickRetryThrowable, TimeSpan.FromSeconds(0.8), 20); + _logger.LogInformation("识别到已经在角色出战界面,等待1.5s"); + Sleep(1500); + } + + /// + /// 获取我方三个角色卡牌区域 + /// + /// + public List GetCharacterRects() + { + Mat srcMat = Capture(); + int halfHeight = srcMat.Height / 2; + Mat bottomMat = new Mat(srcMat, new Rect(0, halfHeight, srcMat.Width, srcMat.Height - halfHeight)); + + var lowPurple = new Scalar(235, 245, 198); + var highPurple = new Scalar(255, 255, 236); + Mat gray = OpenCvCommonHelper.Threshold(bottomMat, lowPurple, highPurple); + + // 水平投影到y轴 正常只有一个连续区域 + int[] h = ArithmeticHelper.HorizontalProjection(gray); + + // y轴 从上到下确认连续区域 + int y1 = 0, y2 = 0; + int start = 0; + bool inLine = false; + for (int i = 0; i < h.Length; i++) + { + if (h[i] > h.Average() * 10) + { + // 直方图 + Cv2.Line(bottomMat, 0, i, h[i], i, Scalar.Yellow); + + if (!inLine) + { + //由空白进入字符区域了,记录标记 + inLine = true; + start = i; + } + } + else if (inLine) + { + //由连续区域进入空白区域了 + inLine = false; + + + if (y1 == 0) + { + y1 = start; + if (OutputImageWhenError) + { + Cv2.Line(bottomMat, 0, y1, bottomMat.Width, y1, Scalar.Red); + } + } + else if (y2 == 0 && i - y1 > 20) + { + y2 = i; + if (OutputImageWhenError) + { + Cv2.Line(bottomMat, 0, y2, bottomMat.Width, y2, Scalar.Red); + } + + break; + } + } + } + + if (y1 == 0 || y2 == 0) + { + _logger.LogWarning("未识别到角色卡牌区域(Y轴)"); + if (OutputImageWhenError) + { + Cv2.ImWrite("logs\\character_card_error.jpg", bottomMat); + } + + throw new RetryException("未获取到角色区域"); + } + + //if (y1 < windowRect.Height / 2 || y2 < windowRect.Height / 2) + //{ + // MyLogger.Warn("识别的角色卡牌区域(Y轴)错误:y1:{} y2:{}", y1, y2); + // if (OutputImageWhenError) + // { + // Cv2.ImWrite("logs\\character_card_error.jpg", bottomMat); + // } + + // throw new RetryException("未获取到角色区域"); + //} + + + // 垂直投影 + int[] v = ArithmeticHelper.VerticalProjection(gray); + + + inLine = false; + start = 0; + List colLines = new List(); + //开始根据投影值识别分割点 + for (int i = 0; i < v.Length; ++i) + { + if (v[i] > h.Average() * 10) + { + if (OutputImageWhenError) + { + Cv2.Line(bottomMat, i, 0, i, v[i], Scalar.Yellow); + } + + if (!inLine) + { + //由空白进入字符区域了,记录标记 + inLine = true; + start = i; + } + } + else if (i - start > 30 && inLine) + { + //由连续区域进入空白区域了 + inLine = false; + if (OutputImageWhenError) + { + Cv2.Line(bottomMat, start, 0, start, bottomMat.Height, Scalar.Red); + } + + colLines.Add(start); + } + } + + if (colLines.Count != 6) + { + _logger.LogWarning("未识别到角色卡牌区域(X轴存在{Count}个识别点)", colLines.Count); + if (OutputImageWhenError) + { + Cv2.ImWrite("logs\\character_card_error.jpg", bottomMat); + } + + throw new RetryException("未获取到角色区域"); + } + + var rects = new List(); + for (int i = 0; i < colLines.Count - 1; i++) + { + if (i % 2 == 0) + { + var r = new Rect(colLines[i], halfHeight + y1, colLines[i + 1] - colLines[i], + y2 - y1); + rects.Add(r); + } + } + + if (rects == null || rects.Count != 3) + { + throw new RetryException("未获取到角色区域"); + } + + //Cv2.ImWrite("logs\\character_card_success.jpg", bottomMat); + return rects; + } + + ///// + ///// 选择角色牌(首次) + ///// + ///// 从左到右第几个角色,从1开始计数 + ///// + //public void ChooseCharacterFirst(int characterIndex) + //{ + // // 首次执行获取角色区域 + // if (MyCharacterRects == null || MyCharacterRects.Count == 0) + // { + // MyCharacterRects = GetCharacterRects(); + // if (MyCharacterRects == null || MyCharacterRects.Count != 3) + // { + // throw new RetryException("未获取到角色区域"); + // } + // } + + + // // 双击选择角色出战 + // DoubleClick(MakeOffset(MyCharacterRects[characterIndex - 1].GetCenterPoint())); + //} + + + ///// + ///// 切换角色牌(历次) + ///// + ///// 从左到右第几个角色,从1开始计数 + ///// + //public bool SwitchCharacterLater(int characterIndex) + //{ + // if (MyCharacterRects == null || MyCharacterRects.Count != 3) + // { + // return false; + // } + + // Point p = MakeOffset(MyCharacterRects[characterIndex - 1].GetCenterPoint()); + // // 选择角色 + // Click(p); + // // // 双击切人 + // // Sleep(1500); + // // Click(p); + + // // 点击切人按钮 + // ActionPhasePressSwitchButton(); + // return true; + //} + + ///// + ///// 角色死亡的时候双击角色牌重新出战 + ///// + ///// 从左到右第几个角色,从1开始计数 + ///// + //public bool SwitchCharacterWhenTakenOut(int characterIndex) + //{ + // if (MyCharacterRects == null || MyCharacterRects.Count != 3) + // { + // return false; + // } + + // Point p = MakeOffset(MyCharacterRects[characterIndex - 1].GetCenterPoint()); + // // 选择角色 + // Click(p); + // // 双击切人 + // Sleep(500); + // Click(p); + // Sleep(300); + // return true; + //} + + + public bool Click(Point p) + { + return false; + } + + public bool Click(int x, int y) + { + return false; + } + + /// + /// 点击捕获区域的相对位置 + /// + /// + /// + /// + public void ClickCaptureArea(int x, int y) + { + var rect = TaskContext.Instance().SystemInfo.CaptureAreaRect; + ClickExtension.Click(rect.X + x, rect.Y + y); + } + + /// + /// 点击游戏屏幕中心点 + /// + public void ClickGameWindowCenter() + { + var p = TaskContext.Instance().SystemInfo.CaptureAreaRect.GetCenterPoint(); + p.Click(); + } + + public static Dictionary> FindMultiPicFromOneImage(Mat srcMat, + Dictionary imgSubDictionary, double threshold = 0.8) + { + var dictionary = new Dictionary>(); + + + foreach (var kvp in imgSubDictionary) + { + dictionary.Add(kvp.Key, MatchTemplateHelper.MatchTemplateMulti(srcMat, kvp.Value, threshold)); + // 最好把结果给遮掩掉,避免识别率不高的时候重复识别 + } + + return dictionary; + } + + /// + /// 重投骰子 + /// + /// 保留的元素类型 + public bool RollPhaseReRoll(params ElementalType[] holdElementalTypes) + { + var gameSnapshot = Capture(); + var dictionary = FindMultiPicFromOneImage(gameSnapshot, _assets.RollPhaseDiceMats); + + var count = dictionary.Sum(kvp => kvp.Value.Count); + + + if (count != 8) + { + _logger.LogDebug("投骰子界面识别到了{Count}个骰子,等待重试", count); + return false; + } + else + { + _logger.LogInformation("投骰子界面识别到了{Count}个骰子", count); + } + + foreach (var kvp in dictionary) + { + // 跳过保留的元素类型 + if (holdElementalTypes.Contains(kvp.Key.ToElementalType())) + { + continue; + } + + // 选中重投 + foreach (var point in kvp.Value) + { + ClickCaptureArea(point.X + _assets.RollPhaseDiceMats[kvp.Key].Width / 2, point.Y + _assets.RollPhaseDiceMats[kvp.Key].Height / 2); + Sleep(100); + } + } + + return true; + } + + /// + /// 选择手牌/重投骰子 确认 + /// + public bool ClickConfirm() + { + var foundRectArea = CaptureRectArea().Find(_assets.ConfirmButtonRo); + if (!foundRectArea.IsEmpty()) + { + foundRectArea.ClickCenter(); + return true; + } + + return false; + } + + public void ReRollDice(params ElementalType[] holdElementalTypes) + { + // 3.重投骰子 + _logger.LogInformation("等待5s投骰动画..."); + + var msg = holdElementalTypes.Aggregate(" ", (current, elementalType) => current + (elementalType.ToChinese() + " ")); + + _logger.LogInformation("保留{Msg}骰子", msg); + Sleep(5000); + var retryCount = 0; + // 保留 x、万能 骰子 + while (!RollPhaseReRoll(holdElementalTypes)) + { + retryCount++; + + if (IsDuelEnd()) + { + throw new DuelEndException("对战已结束,停止自动打牌!"); + } + + //MyLogger.Debug("识别骰子数量不正确,第{}次重试中...", retryCount); + Sleep(500); + if (retryCount > 35) + { + throw new System.Exception("识别骰子数量不正确,重试超时,停止自动打牌!"); + } + } + + ClickConfirm(); + _logger.LogInformation("选择需要重投的骰子后点击确认完毕"); + + Sleep(1000); + // 鼠标移动到中心 + ClickGameWindowCenter(); + + _logger.LogInformation("等待10s对方重投"); + Sleep(10000); + } + + public Point MakeOffset(Point p) + { + var rect = TaskContext.Instance().SystemInfo.CaptureAreaRect; + return new Point(rect.X + p.X, rect.Y + p.Y); + } + + /// + /// 计算当前有那些骰子 + /// + /// + public Dictionary ActionPhaseDice() + { + var srcMat = Capture(); + // 切割图片后再识别 加快速度 位置没啥用,所以切割后比较方便 + var dictionary = FindMultiPicFromOneImage(CutRight(srcMat, srcMat.Width / 5), _assets.ActionPhaseDiceMats); + + var msg = ""; + var result = new Dictionary(); + foreach (var kvp in dictionary) + { + result.Add(kvp.Key, kvp.Value.Count); + msg += $"{kvp.Key.ToElementalType().ToChinese()} {kvp.Value.Count}| "; + } + + _logger.LogInformation("当前骰子状态:{Res}", result); + return result; + } + + + /// + /// 烧牌 + /// + public void ActionPhaseElementalTuning() + { + var rect = TaskContext.Instance().SystemInfo.CaptureAreaRect; + var m = ClickExtension.Move(rect.X + rect.Width / 2d, rect.Y + rect.Height - 50).LeftButtonClick(); + Sleep(1500); + m.LeftButtonDown(); + Sleep(100); + m = ClickExtension.Move(rect.X + rect.Width - 50, rect.Y + rect.Height / 2d); + Sleep(100); + m.LeftButtonUp(); + } + + /// + /// 烧牌确认(元素调和按钮) + /// + public bool ActionPhaseElementalTuningConfirm() + { + var foundRectArea = CaptureRectArea().Find(_assets.ElementalTuningConfirmButtonRo); + if (!foundRectArea.IsEmpty()) + { + foundRectArea.ClickCenter(); + return true; + } + + return false; + } + + /// + /// 点击切人按钮 + /// + /// + public void ActionPhasePressSwitchButton() + { + var info = TaskContext.Instance().SystemInfo; + var x = info.CaptureAreaRect.X + info.CaptureAreaRect.Width - 100 * info.AssetScale; + var y = info.CaptureAreaRect.Y + info.CaptureAreaRect.Height - 120 * info.AssetScale; + + ClickExtension.Move(x, y).LeftButtonClick(); + Sleep(800); // 等待动画彻底弹出 + + ClickExtension.Move(x, y).LeftButtonClick(); + } + + + /// + /// 使用技能 + /// + /// 技能编号,从右往左数,从1开始 + /// 元素骰子是否充足 + public bool ActionPhaseUseSkill(int skillIndex) + { + // 技能坐标写死 (w - 100 * n, h - 120) + var info = TaskContext.Instance().SystemInfo; + var x = info.CaptureAreaRect.X + info.CaptureAreaRect.Width - 100 * info.AssetScale * skillIndex; + var y = info.CaptureAreaRect.Y + info.CaptureAreaRect.Height - 120 * info.AssetScale; + ClickExtension.Move(x, y).LeftButtonClick(); + Sleep(1000); // 等待动画彻底弹出 + + CaptureRectArea().Find(_assets.ElementalDiceLackWarningRo, foundRectArea => + { + // 多点几次保证点击到 + _logger.LogInformation("使用技能{SkillIndex}", skillIndex); + foundRectArea.ClickCenter(); + Sleep(200); + foundRectArea.ClickCenter(); + }); + + return true; + } + + /// + /// 使用技能(元素骰子不够的情况下,自动烧牌) + /// + /// 技能编号,从右往左数,从1开始 + /// 技能消耗骰子数 + /// 消耗骰子元素类型 + /// 对局对象 + /// 手牌或者元素骰子是否充足 + public bool ActionPhaseAutoUseSkill(int skillIndex, int diceCost, ElementalType elementalType, Duel duel) + { + var dice9RetryCount = 0; + var retryCount = 0; + var diceStatus = ActionPhaseDice(); + while (true) + { + int dCount = diceStatus.Sum(x => x.Value); + if (dCount != duel.CurrentDiceCount) + { + if (retryCount > 20) + { + throw new System.Exception("骰子数量与预期不符,重试次数过多,可能出现了未知错误!"); + } + + if (dCount == 9 && duel.CurrentDiceCount == 8 && diceStatus[ElementalType.Omni.ToLowerString()] > 0) + { + dice9RetryCount++; + if (dice9RetryCount > 5) + { + // 支援区存在 鲸井小弟 情况下骰子数量增加导致识别出错的问题 #1 + // 5次重试后仍然是9个骰子并且至少有一个万能骰子,出现多识别的情况是很稀少的,此时可以基本认为 支援区存在 鲸井小弟 + // TODO : 但是这个方法并不是100%准确,后续需要添加支援区判断 + _logger.LogInformation("期望的骰子数量8,应为开局期望,重试多次后累计实际识别9个骰子的情况为5次"); + duel.CurrentDiceCount = 9; // 修正当前骰子数量 + break; + } + } + + + _logger.LogInformation("当前骰子数量{Count}与期望的骰子数量{Expect}不相等,重试", dCount, duel.CurrentDiceCount); + diceStatus = ActionPhaseDice(); + retryCount++; + Sleep(1000); + } + else + { + break; + } + } + + + int needSpecifyElementDiceCount = diceCost - diceStatus[ElementalType.Omni.ToLowerString()] - diceStatus[elementalType.ToLowerString()]; + if (needSpecifyElementDiceCount > 0) + { + if (duel.CurrentCardCount < needSpecifyElementDiceCount) + { + _logger.LogInformation("当前手牌数{Current}小于需要烧牌数量{Expect},无法释放技能", duel.CurrentCardCount, needSpecifyElementDiceCount); + return false; + } + + _logger.LogInformation("当前需要的元素骰子数量不足{Cost}个,还缺{Lack}个,当前手牌数{Current},烧牌", diceCost, needSpecifyElementDiceCount, duel.CurrentCardCount); + + for (var i = 0; i < needSpecifyElementDiceCount; i++) + { + duel.CurrentCardCount--; + _logger.LogInformation("- {Count} 烧牌", i + 1); + ActionPhaseElementalTuning(); + Sleep(100); + ActionPhaseElementalTuningConfirm(); + Sleep(1000); // 烧牌动画 + ClickGameWindowCenter(); // 复位 + Sleep(500); + // 最后一张牌的回正速度较慢,多等一会 + if (duel.CurrentCardCount <= 1) + { + ClickGameWindowCenter(); // 复位 + Sleep(1000); + } + } + } + + return ActionPhaseUseSkill(skillIndex); + } + + + /// + /// 回合结束 + /// + public void RoundEnd() + { + CaptureRectArea().Find(_assets.RoundEndButtonRo, foundRectArea => + { + foundRectArea.ClickCenter(); + Sleep(1000); // 有弹出动画 + foundRectArea.ClickCenter(); + Sleep(300); + }); + + ClickGameWindowCenter(); // 复位 + } + + /// + /// 是否是再角色出战选择界面 + /// 可重试方法 + /// + public void IsInCharacterPickRetryThrowable() + { + if (!IsInCharacterPick()) + { + throw new RetryException("当前不在角色出战选择界面"); + } + } + + /// + /// 是否是再角色出战选择界面 + /// + /// + public bool IsInCharacterPick() + { + return !CaptureRectArea().Find(_assets.InCharacterPickRo).IsEmpty(); + } + + /// + /// 是否是我的回合 + /// + /// + public bool IsInMyAction() + { + return !CaptureRectArea().Find(_assets.RoundEndButtonRo).IsEmpty(); + } + + /// + /// 是否是对方的回合 + /// + /// + public bool IsInOpponentAction() + { + return !CaptureRectArea().Find(_assets.InOpponentActionRo).IsEmpty(); + } + + /// + /// 是否是回合结算阶段 + /// + /// + public bool IsEndPhase() + { + return !CaptureRectArea().Find(_assets.EndPhaseRo).IsEmpty(); + } + + + /// + /// 出战角色是否被打倒 + /// + /// + public bool IsActiveCharacterTakenOut() + { + return !CaptureRectArea().Find(_assets.CharacterTakenOutRo).IsEmpty(); + } + + /// + /// 哪些出战角色被打倒了 + /// + /// true 是已经被打倒 + public bool[] WhatCharacterDefeated(List rects) + { + if (rects == null || rects.Count != 3) + { + throw new System.Exception("未能获取到我方角色卡位置"); + } + + var pList = MatchTemplateHelper.MatchTemplateMulti(Capture(), _assets.CharacterDefeatedMat, 0.8); + + var res = new bool[3]; + foreach (var p in pList) + { + for (var i = 0; i < rects.Count; i++) + { + if (IsOverlap(rects[i], new Rect(p.X, p.Y, _assets.CharacterDefeatedMat.Width, _assets.CharacterDefeatedMat.Height))) + { + res[i] = true; + } + } + } + + + return res; + } + + /// + /// 判断矩形是否重叠 + /// + /// + /// + /// + public bool IsOverlap(Rect rc1, Rect rc2) + { + if (rc1.X + rc1.Width > rc2.X && + rc2.X + rc2.Width > rc1.X && + rc1.Y + rc1.Height > rc2.Y && + rc2.Y + rc2.Height > rc1.Y + ) + { + return true; + } + else + { + return false; + } + } + + /// + /// 是否对局完全结束 + /// + /// + public bool IsDuelEnd() + { + return !CaptureRectArea().Find(_assets.ExitDuelButtonRo).IsEmpty(); + } + + + public Mat CutRight(Mat srcMat, int saveRightWidth) + { + srcMat = new Mat(srcMat, new Rect(srcMat.Width - saveRightWidth, 0, saveRightWidth, srcMat.Height)); + return srcMat; + } + + /// + /// 等待我的回合 + /// 我方角色可能在此期间阵亡 + /// + public void WaitForMyTurn(Duel duel, int waitTime = 0) + { + if (waitTime > 0) + { + _logger.LogInformation("等待对方行动{Time}s", waitTime / 1000); + Sleep(waitTime); + } + + // 判断对方行动是否已经结束 + var retryCount = 0; + var inMyActionCount = 0; + while (true) + { + if (IsInMyAction()) + { + if (IsActiveCharacterTakenOut()) + { + DoWhenCharacterDefeated(duel); + } + else + { + // 多延迟2s // 保证被击败提示已经完成显示 + inMyActionCount++; + if (inMyActionCount == 3) + { + break; + } + } + } + else if (IsDuelEnd()) + { + throw new DuelEndException("对战已结束,停止自动打牌!"); + } + + retryCount++; + if (retryCount >= 60) + { + throw new System.Exception("等待对方行动超时,停止自动打牌!"); + } + + _logger.LogInformation("对方仍在行动中,继续等待(次数{})...", retryCount); + Sleep(1000); + } + } + + /// + /// 等待对方回合 和 回合结束阶段 + /// 我方角色可能在此期间阵亡 + /// + public void WaitOpponentAction(Duel duel) + { + var rd = new Random(); + Sleep(3000 + rd.Next(1, 1000)); + // 判断对方行动是否已经结束 + var retryCount = 0; + while (true) + { + if (IsInOpponentAction()) + { + _logger.LogInformation("对方仍在行动中,继续等待(次数{})...", retryCount); + } + else if (IsEndPhase()) + { + _logger.LogInformation("正在回合结束阶段,继续等待(次数{})...", retryCount); + } + else if (IsInMyAction()) + { + if (IsActiveCharacterTakenOut()) + { + DoWhenCharacterDefeated(duel); + } + } + else if (IsDuelEnd()) + { + throw new DuelEndException("对战已结束,停止自动打牌!"); + } + else + { + // 至少走三次判断才能确定对方行动结束 + if (retryCount > 2) + { + break; + } + else + { + _logger.LogError("等待对方回合 和 回合结束阶段 时程序未识别到有效内容(次数{})...", retryCount); + } + } + + retryCount++; + if (retryCount >= 30) + { + throw new System.Exception("等待对方行动超时,停止自动打牌!"); + } + + + Sleep(1000 + rd.Next(1, 500)); + } + } + + /// + /// 角色被打败后要切换角色 + /// + /// + /// + public void DoWhenCharacterDefeated(Duel duel) + { + _logger.LogInformation("当前出战角色被打败,需要选择新的出战角色"); + var defeatedArray = WhatCharacterDefeated(duel.CharacterCardRects); + + for (var i = defeatedArray.Length - 1; i >= 0; i--) + { + duel.Characters[i + 1].IsDefeated = defeatedArray[i]; + } + + var orderList = duel.GetCharacterSwitchOrder(); + if (orderList.Count == 0) + { + throw new DuelEndException("后续行动策略中,已经没有可切换且存活的角色了,结束自动打牌(建议添加更多行动)"); + } + + foreach (var j in orderList) + { + if (!duel.Characters[j].IsDefeated) + { + duel.Characters[j].SwitchWhenTakenOut(); + break; + } + } + + ClickGameWindowCenter(); + Sleep(2000); // 切人动画 + } + + + ///// + ///// 哪个角色处于出战状态 + ///// + ///// + //public Character WhichCharacterActive(Duel duel) + //{ + // if (duel.CharacterCardRects == null || duel.CharacterCardRects.Count != 3) + // { + // throw new System.Exception("未能获取到我方角色卡位置"); + // } + + // Mat srcMat = Capture(); + + // // 切割下半部分 + // int halfHeight = srcMat.Height / 2; + // Mat bottomMat = new Mat(srcMat, new Rect(0, halfHeight, srcMat.Width, srcMat.Height - halfHeight)); + // Mat resMat; + // List pList = ImageRecognition.FindMultiTarget(bottomMat, + // ImageResCollections.CharacterHpUpperBitmap.ToMat(), "HpUpper", out resMat, 0.7); + + // if (pList.Count != duel.GetCharacterAliveNum()) + // { + // if (OutputImageWhenError) + // { + // var outMat = srcMat.Clone(); + // foreach (var point in pList) + // { + // Cv2.Rectangle(outMat, + // new Rect(point.X, point.Y + halfHeight, ImageResCollections.CharacterHpUpperBitmap.Width, + // ImageResCollections.CharacterHpUpperBitmap.Height), Scalar.Red, 2); + // } + + // Cv2.ImWrite("logs\\active_character_error.jpg", outMat); + // } + + // throw new RetryException($"角色Hp区块识别有误,识别到区块数量{pList.Count} != 当前存活角色数{duel.GetCharacterAliveNum()}"); + // } + + // int cnt = 0; + // for (var i = 1; i < duel.Characters.Length; i++) + // { + // var cardRect = duel.Characters[i].Area; + // // 2倍高度 保证能够矩形相交 + // var rect1 = new Rectangle(cardRect.X, cardRect.Y - cardRect.Height, cardRect.Width, + // cardRect.Height + cardRect.Height); + + // foreach (var point in pList) + // { + // var rect2 = new Rectangle(point.X, halfHeight + point.Y, + // ImageResCollections.CharacterHpUpperBitmap.Width, + // ImageResCollections.CharacterHpUpperBitmap.Height); + // if (IsOverlap(rect1, rect2)) + // { + // duel.Characters[i].HpUpperArea = rect2; + // // 出战角色判断 + // if (halfHeight + point.Y < cardRect.Y) + // { + // cnt++; + // duel.CurrentCharacter = duel.Characters[i]; + // } + + // break; + // } + // } + // } + + // if (cnt != 1) + // { + // if (OutputImageWhenError) + // { + // var outMat = srcMat.Clone(); + // foreach (var point in pList) + // { + // Cv2.Rectangle(outMat, + // new Rect(point.X, point.Y + halfHeight, ImageResCollections.CharacterHpUpperBitmap.Width, + // ImageResCollections.CharacterHpUpperBitmap.Height), Scalar.Red, 2); + // } + + // foreach (var rc in duel.CharacterCardRects) + // { + // Cv2.Rectangle(outMat, + // rc.ToCvRect(), Scalar.Green, 2); + // } + + // Cv2.ImWrite("logs\\active_character_error.jpg", outMat); + // } + + // throw new RetryException($"识别到{cnt}个出战角色"); + // } + + // AppendCharacterStatus(duel.CurrentCharacter, srcMat); + // return duel.CurrentCharacter; + //} + + public void AppendCharacterStatus(Character character, Mat srcMat) + { + // 截取出战角色区域扩展 + var characterMat = new Mat(srcMat, new Rect(character.Area.X, + character.Area.Y, + character.Area.Width + 40, + character.Area.Height + 10)); + // 识别角色异常状态 + var pCharacterStatusFreeze = MatchTemplateHelper.MatchTemplate(characterMat, _assets.CharacterStatusFreezeMat, TemplateMatchModes.CCoeffNormed); + if (pCharacterStatusFreeze != new Point()) + { + character.StatusList.Add(CharacterStatusEnum.Frozen); + } + var pCharacterStatusDizziness = MatchTemplateHelper.MatchTemplate(characterMat, _assets.CharacterStatusDizzinessMat, TemplateMatchModes.CCoeffNormed); + if (pCharacterStatusDizziness != new Point()) + { + character.StatusList.Add(CharacterStatusEnum.Frozen); + } + + // 识别角色能量 + var energyPointList = MatchTemplateHelper.MatchTemplateMulti(characterMat.Clone(),_assets.CharacterEnergyOnMat, 0.8); + character.EnergyByRecognition = energyPointList.Count; + + _logger.LogInformation("当前出战{Character}", character); + } + + public Character WhichCharacterActiveWithRetry(Duel duel) + { + // 检查角色是否被击败 // 这里又检查一次是因为最后一个角色存活的情况下,会自动出战 + var defeatedArray = WhatCharacterDefeated(duel.CharacterCardRects); + for (var i = defeatedArray.Length - 1; i >= 0; i--) + { + duel.Characters[i + 1].IsDefeated = defeatedArray[i]; + } + + return Retry.Do(() => WhichCharacterActiveByHpWord(duel), TimeSpan.FromSeconds(0.3), 2); + } + + public Character WhichCharacterActiveByHpWord(Duel duel) + { + if (duel.CharacterCardRects == null || duel.CharacterCardRects.Count != 3) + { + throw new System.Exception("未能获取到我方角色卡位置"); + } + + var srcMat = Capture(); + + int halfHeight = srcMat.Height / 2; + Mat bottomMat = new Mat(srcMat, new Rect(0, halfHeight, srcMat.Width, srcMat.Height - halfHeight)); + + var lowPurple = new Scalar(239, 239, 239); + var highPurple = new Scalar(242, 242, 250); + Mat gray = OpenCvCommonHelper.Threshold(bottomMat, lowPurple, highPurple); + + var kernel = Cv2.GetStructuringElement(MorphShapes.Rect, new OpenCvSharp.Size(15, 10), new OpenCvSharp.Point(-1, -1)); + Cv2.Dilate(gray, gray, kernel); //膨胀 + + + OpenCvSharp.Point[][] contours; + HierarchyIndex[] hierarchy; + Cv2.FindContours(gray, out contours, out hierarchy, RetrievalModes.External, + ContourApproximationModes.ApproxSimple, null); + + if (contours.Length > 0) + { + // .Where(w => w.Width > 1 && w.Height >= 5) + var rects = contours.Select(Cv2.BoundingRect).ToList(); + + + // 按照Y轴高度排序 + rects = rects.OrderBy(r => r.Y).ToList(); + + // 第一个和角色卡重叠的矩形 + foreach (var rect in rects) + { + for (var i = 0; i < duel.CharacterCardRects.Count; i++) + { + // 延长高度,确保能够相交 + var rect1 = new Rect(rect.X, halfHeight + rect.Y, rect.Width + 20, + rect.Height + 20); + if (IsOverlap(rect1, duel.CharacterCardRects[i]) && + halfHeight + rect.Y < duel.CharacterCardRects[i].Y) + { + // 首个相交矩形就是出战角色 + duel.CurrentCharacter = duel.Characters[i + 1]; + AppendCharacterStatus(duel.CurrentCharacter, srcMat); + + Cv2.Rectangle(srcMat, rect1, Scalar.Yellow); + Cv2.Rectangle(srcMat, duel.CharacterCardRects[i], Scalar.Blue, 2); + OutputImage(duel, rects, bottomMat, halfHeight, "logs\\active_character2_success.jpg"); + return duel.CurrentCharacter; + } + } + } + + OutputImage(duel, rects, bottomMat, halfHeight, "logs\\active_character2_no_overlap_error.jpg"); + } + else + { + if (OutputImageWhenError) + { + Cv2.ImWrite("logs\\active_character2_no_rects_error.jpg", gray); + } + } + + throw new RetryException($"未识别到个出战角色"); + } + + private static void OutputImage(Duel duel, List rects, Mat bottomMat, int halfHeight, string fileName) + { + if (OutputImageWhenError) + { + foreach (var rect2 in rects) + { + Cv2.Rectangle(bottomMat, new OpenCvSharp.Point(rect2.X, rect2.Y), + new OpenCvSharp.Point(rect2.X + rect2.Width, rect2.Y + rect2.Height), Scalar.Red, 1); + } + + foreach (var rc in duel.CharacterCardRects) + { + Cv2.Rectangle(bottomMat, + new Rect(rc.X, rc.Y - halfHeight, rc.Width, rc.Height), Scalar.Green, 1); + } + + + Cv2.ImWrite(fileName, bottomMat); + } + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/ActionCommand.cs b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/ActionCommand.cs new file mode 100644 index 00000000..b5b59016 --- /dev/null +++ b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/ActionCommand.cs @@ -0,0 +1,82 @@ +using System; + +namespace BetterGenshinImpact.GameTask.AutoGeniusInvokation.Model +{ + public class ActionCommand + { + /// + /// 角色 + /// + public Character Character { get; set; } + + public ActionEnum Action { get; set; } + + /// + /// 目标编号(技能编号,从右往左) + /// + public int TargetIndex { get; set; } + + public override string ToString() + { + if (Action == ActionEnum.UseSkill) + { + return $"【{Character.Name}】使用【技能{TargetIndex}】"; + } + else if (Action == ActionEnum.SwitchLater) + { + return $"【{Character.Name}】切换至【角色{TargetIndex}】"; + } + else + { + return base.ToString(); + } + } + + + public int GetSpecificElementDiceUseCount() + { + if (Action == ActionEnum.UseSkill) + { + return Character.Skills[TargetIndex].SpecificElementCost; + } + else + { + throw new ArgumentException("未知行动"); + } + } + + public int GetAllDiceUseCount() + { + if (Action == ActionEnum.UseSkill) + { + return Character.Skills[TargetIndex].AllCost; + } + else + { + throw new ArgumentException("未知行动"); + } + } + + public ElementalType GetDiceUseElementType() + { + if (Action == ActionEnum.UseSkill) + { + return Character.Element; + } + else + { + throw new ArgumentException("未知行动"); + } + } + + public bool SwitchLater() + { + return Character.SwitchLater(); + } + + public bool UseSkill(Duel duel) + { + return Character.UseSkill(TargetIndex, duel); + } + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/ActionEnum.cs b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/ActionEnum.cs new file mode 100644 index 00000000..565e8a8c --- /dev/null +++ b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/ActionEnum.cs @@ -0,0 +1,45 @@ +using System; + +namespace BetterGenshinImpact.GameTask.AutoGeniusInvokation.Model +{ + public enum ActionEnum + { + ChooseFirst, SwitchLater, UseSkill + } + + public static class ActionEnumExtension + { + public static ActionEnum ChineseToActionEnum(this string type) + { + type = type.ToLower(); + switch (type) + { + case "出战": + //return ActionEnum.ChooseFirst; + throw new ArgumentOutOfRangeException(nameof(type), type, null); + case "切换": + //return ActionEnum.SwitchLater; + throw new ArgumentOutOfRangeException(nameof(type), type, null); + case "使用": + return ActionEnum.UseSkill; + default: + throw new ArgumentOutOfRangeException(nameof(type), type, null); + } + } + + public static string ToChinese(this ActionEnum type) + { + switch (type) + { + case ActionEnum.ChooseFirst: + return "出战"; + case ActionEnum.SwitchLater: + return "切换"; + case ActionEnum.UseSkill: + return "使用"; + default: + throw new ArgumentOutOfRangeException(nameof(type), type, null); + } + } + } +} diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/Character.cs b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/Character.cs new file mode 100644 index 00000000..6e990099 --- /dev/null +++ b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/Character.cs @@ -0,0 +1,115 @@ +using BetterGenshinImpact.Core.Recognition.OpenCv; +using BetterGenshinImpact.Helpers.Extensions; +using OpenCvSharp; +using System.Collections.Generic; +using System.Text; +using Microsoft.Extensions.Logging; + +namespace BetterGenshinImpact.GameTask.AutoGeniusInvokation.Model +{ + public class Character + { + private readonly ILogger _logger = App.GetLogger(); + + /// + /// 1-3 所在数组下标一致 + /// + public int Index { get; set; } + + public string Name { get; set; } + public ElementalType Element { get; set; } + public Skill[] Skills { get; set; } + + + /// + /// 是否被打败 + /// + public bool IsDefeated { get; set; } + + /// + /// 充能点 + /// + public int Energy { get; set; } + + + /// + /// 充能点来自于图像识别 + /// + public int EnergyByRecognition { get; set; } + + /// + /// 角色身上的负面状态 + /// + public List StatusList { get; set; } = new List(); + + /// + /// 角色区域 + /// + public Rect Area { get; set; } + + /// + /// 血量上方区域,用于判断是否出战 + /// + public Rect HpUpperArea { get; set; } + + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + sb.Append($"角色{Index},"); + sb.Append($"充能={EnergyByRecognition},"); + if (StatusList?.Count > 0) + { + sb.Append($"状态:{string.Join(",", StatusList)}"); + } + + return sb.ToString(); + } + + public void ChooseFirst() + { + ClickExtension.Move(GeniusInvokationControl.GetInstance().MakeOffset(Area.GetCenterPoint())).LeftButtonDoubleClick(); + } + + public bool SwitchLater() + { + var p = GeniusInvokationControl.GetInstance().MakeOffset(Area.GetCenterPoint()); + // 选择角色 + ClickExtension.Move(p).LeftButtonClick(); + + // 点击切人按钮 + GeniusInvokationControl.GetInstance().ActionPhasePressSwitchButton(); + return true; + } + + /// + /// 角色被打败的时候双击角色牌重新出战 + /// + /// + public void SwitchWhenTakenOut() + { + _logger.LogInformation("有角色被打败,当前选择{Name}出战", Name); + var p = GeniusInvokationControl.GetInstance().MakeOffset(Area.GetCenterPoint()); + // 选择角色 + ClickExtension.Move(p).LeftButtonClick(); + // 双击切人 + GeniusInvokationControl.GetInstance().Sleep(500); + ClickExtension.Move(p).LeftButtonClick(); + GeniusInvokationControl.GetInstance().Sleep(300); + } + + public bool UseSkill(int skillIndex, Duel duel) + { + bool res = GeniusInvokationControl.GetInstance() + .ActionPhaseAutoUseSkill(skillIndex, Skills[skillIndex].SpecificElementCost, Skills[skillIndex].Type, duel); + if (res) + { + return true; + } + else + { + _logger.LogWarning("没有足够的手牌或元素骰子释放技能"); + return false; + } + } + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/CharacterStatusEnum.cs b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/CharacterStatusEnum.cs new file mode 100644 index 00000000..f193d3f4 --- /dev/null +++ b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/CharacterStatusEnum.cs @@ -0,0 +1,8 @@ +namespace BetterGenshinImpact.GameTask.AutoGeniusInvokation.Model +{ + public enum CharacterStatusEnum + { + Frozen, + Dizziness + } +} diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/Duel.cs b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/Duel.cs new file mode 100644 index 00000000..0edbd106 --- /dev/null +++ b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/Duel.cs @@ -0,0 +1,382 @@ +using BetterGenshinImpact.GameTask.AutoGeniusInvokation.Exception; +using GeniusInvokationAutoToy.Utils; +using Microsoft.Extensions.Logging; +using OpenCvSharp; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace BetterGenshinImpact.GameTask.AutoGeniusInvokation.Model; + +public class Duel +{ + private readonly ILogger _logger = App.GetLogger(); + + public Character CurrentCharacter { get; set; } + public Character[] Characters { get; set; } = new Character[4]; + + /// + /// 行动指令队列 + /// + public List ActionCommandQueue { get; set; } = new List(); + + /// + /// 当前回合数 + /// + public int RoundNum { get; set; } = 1; + + /// + /// 角色牌位置 + /// + public List CharacterCardRects { get; set; } + + /// + /// 手牌数量 + /// + public int CurrentCardCount { get; set; } = 0; + + /// + /// 骰子数量 + /// + public int CurrentDiceCount { get; set; } = 0; + + + public CancellationTokenSource Cts { get; set; } + + + public async Task CustomStrategyRunAsync(CancellationTokenSource cts1) + { + await Task.Run(() => { CustomStrategyRun(cts1); }); + } + + public void CustomStrategyRun(CancellationTokenSource cts1) + { + Cts = cts1; + try + { + _logger.LogInformation("========================================"); + _logger.LogInformation("→ {Text}", "全自动七圣召唤,启动!"); + + GeniusInvokationControl.GetInstance().Init(Cts); + SystemControl.ActivateWindow(); + + // 对局准备 选择初始手牌 + GeniusInvokationControl.GetInstance().CommonDuelPrepare(); + + + // 获取角色区域 + CharacterCardRects = Retry.Do(() => GeniusInvokationControl.GetInstance().GetCharacterRects(), + TimeSpan.FromSeconds(1.5), 20); + if (CharacterCardRects == null || CharacterCardRects.Count != 3) + { + throw new DuelEndException("未成功获取到角色区域"); + } + + for (var i = 1; i < 4; i++) + { + Characters[i].Area = CharacterCardRects[i - 1]; + } + + // 出战角色 + CurrentCharacter = ActionCommandQueue[0].Character; + CurrentCharacter.ChooseFirst(); + + // 开始执行回合 + while (true) + { + _logger.LogInformation("--------------第{RoundNum}回合--------------", RoundNum); + ClearCharacterStatus(); // 清空单回合的异常状态 + if (RoundNum == 1) + { + CurrentCardCount = 5; + } + else + { + CurrentCardCount += 2; + } + + CurrentDiceCount = 8; + + // 预计算本回合内的所有可能的元素 + var elementSet = PredictionDiceType(); + + // 0 投骰子 + GeniusInvokationControl.GetInstance().ReRollDice(elementSet.ToArray()); + + // 等待到我的回合 // 投骰子动画时间是不确定的 // 可能是对方先手 + GeniusInvokationControl.GetInstance().WaitForMyTurn(this, 1000); + + // 开始执行行动 + while (true) + { + // 没骰子了就结束行动 + _logger.LogInformation("行动开始,当前骰子数[{CurrentDiceCount}],当前手牌数[{CurrentCardCount}]", CurrentDiceCount, CurrentCardCount); + if (CurrentDiceCount <= 0) + { + _logger.LogInformation("骰子已经用完"); + GeniusInvokationControl.GetInstance().Sleep(2000); + break; + } + + // 每次行动前都要检查当前角色 + CurrentCharacter = GeniusInvokationControl.GetInstance().WhichCharacterActiveWithRetry(this); + + var alreadyExecutedActionIndex = new List(); + var alreadyExecutedActionCommand = new List(); + var i = 0; + for (i = 0; i < ActionCommandQueue.Count; i++) + { + var actionCommand = ActionCommandQueue[i]; + // 指令中的角色未被打败、角色有异常状态 跳过指令 + if (actionCommand.Character.IsDefeated || actionCommand.Character.StatusList?.Count > 0) + { + continue; + } + + // 当前出战角色身上存在异常状态的情况下不执行本角色的指令 + if (CurrentCharacter.StatusList?.Count > 0 && + actionCommand.Character.Index == CurrentCharacter.Index) + { + continue; + } + + + // 1. 判断切人 + if (CurrentCharacter.Index != actionCommand.Character.Index) + { + if (CurrentDiceCount >= 1) + { + actionCommand.SwitchLater(); + CurrentDiceCount--; + alreadyExecutedActionIndex.Add(-actionCommand.Character.Index); // 标记为已执行 + var switchAction = new ActionCommand + { + Character = CurrentCharacter, + Action = ActionEnum.SwitchLater, + TargetIndex = actionCommand.Character.Index + }; + alreadyExecutedActionCommand.Add(switchAction); + _logger.LogInformation("→指令执行完成:{Action}", switchAction); + break; + } + else + { + _logger.LogInformation("骰子不足以进行下一步:切换角色 {CharacterIndex}", actionCommand.Character.Index); + break; + } + } + + // 2. 判断使用技能 + if (actionCommand.GetAllDiceUseCount() > CurrentDiceCount) + { + _logger.LogInformation("骰子不足以进行下一步:{Action}", actionCommand); + break; + } + else + { + bool useSkillRes = actionCommand.UseSkill(this); + if (useSkillRes) + { + CurrentDiceCount -= actionCommand.GetAllDiceUseCount(); + alreadyExecutedActionIndex.Add(i); + alreadyExecutedActionCommand.Add(actionCommand); + _logger.LogInformation("→指令执行完成:{Action}", actionCommand); + } + else + { + _logger.LogWarning("→指令执行失败(可能是手牌不够):{Action}", actionCommand); + GeniusInvokationControl.GetInstance().Sleep(1000); + GeniusInvokationControl.GetInstance().ClickGameWindowCenter(); + } + + break; + } + } + + + + if (alreadyExecutedActionIndex.Count != 0) + { + foreach (var index in alreadyExecutedActionIndex) + { + if (index >= 0) + { + ActionCommandQueue.RemoveAt(index); + } + } + + alreadyExecutedActionIndex.Clear(); + // 等待对方行动完成 (开大的时候等待时间久一点) + var sleepTime = ComputeWaitForMyTurnTime(alreadyExecutedActionCommand); + GeniusInvokationControl.GetInstance().WaitForMyTurn(this, sleepTime); + alreadyExecutedActionCommand.Clear(); + } + else + { + // 如果没有任何指令可以执行 则跳出循环 + // TODO 也有可能是角色死亡/所有角色被冻结导致没有指令可以执行 + //if (i >= ActionCommandQueue.Count) + //{ + // throw new DuelEndException("策略中所有指令已经执行完毕,结束自动打牌"); + //} + GeniusInvokationControl.GetInstance().Sleep(2000); + break; + } + + if (ActionCommandQueue.Count == 0) + { + throw new DuelEndException("策略中所有指令已经执行完毕,结束自动打牌"); + } + } + + // 回合结束 + GeniusInvokationControl.GetInstance().Sleep(1000); + _logger.LogInformation("我方点击回合结束"); + GeniusInvokationControl.GetInstance().RoundEnd(); + + // 等待对方行动+回合结算 + GeniusInvokationControl.GetInstance().WaitOpponentAction(this); + RoundNum++; + } + } + catch (TaskCanceledException ex) + { + _logger.LogInformation(ex.Message); + } + catch (DuelEndException ex) + { + _logger.LogInformation(ex.Message); + _logger.LogInformation("← {Text}", "退出全自动七圣召唤"); + } + catch (System.Exception ex) + { + _logger.LogError(ex.ToString()); + } + finally + { + _logger.LogInformation("========================================"); + } + } + + private HashSet PredictionDiceType() + { + var actionUseDiceSum = 0; + var elementSet = new HashSet + { + ElementalType.Omni + }; + for (var i = 0; i < ActionCommandQueue.Count; i++) + { + var actionCommand = ActionCommandQueue[i]; + + // 角色未被打败的情况下才能执行 + if (actionCommand.Character.IsDefeated) + { + continue; + } + + // 通过骰子数量判断是否可以执行 + + // 1. 判断切人 + if (i > 0 && actionCommand.Character.Index != ActionCommandQueue[i - 1].Character.Index) + { + actionUseDiceSum++; + if (actionUseDiceSum > CurrentDiceCount) + { + break; + } + else + { + elementSet.Add(actionCommand.GetDiceUseElementType()); + //executeActionIndex.Add(-actionCommand.Character.Index); + } + } + + // 2. 判断使用技能 + actionUseDiceSum += actionCommand.GetAllDiceUseCount(); + if (actionUseDiceSum > CurrentDiceCount) + { + break; + } + else + { + elementSet.Add(actionCommand.GetDiceUseElementType()); + //executeActionIndex.Add(i); + } + } + + return elementSet; + } + + public void ClearCharacterStatus() + { + foreach (var character in Characters) + { + character?.StatusList?.Clear(); + } + } + + /// + /// 根据前面执行的命令计算等待时间 + /// 大招等待15秒 + /// 快速切换等待3秒 + /// + /// + /// + private int ComputeWaitForMyTurnTime(List alreadyExecutedActionCommand) + { + foreach (var command in alreadyExecutedActionCommand) + { + if (command.Action == ActionEnum.UseSkill && command.TargetIndex == 1) + { + return 15000; + } + + // 莫娜切换等待3秒 + if (command.Character.Name == "莫娜" && command.Action == ActionEnum.SwitchLater) + { + return 3000; + } + } + + return 10000; + } + + /// + /// 获取角色切换顺序 + /// + /// + public List GetCharacterSwitchOrder() + { + List orderList = new List(); + for (var i = 0; i < ActionCommandQueue.Count; i++) + { + if (!orderList.Contains(ActionCommandQueue[i].Character.Index)) + { + orderList.Add(ActionCommandQueue[i].Character.Index); + } + } + + return orderList; + } + + /// + /// 获取角色存活数量 + /// + /// + public int GetCharacterAliveNum() + { + int num = 0; + foreach (var character in Characters) + { + if (character != null && !character.IsDefeated) + { + num++; + } + } + + return num; + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/ElementalType.cs b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/ElementalType.cs new file mode 100644 index 00000000..1e72d0ee --- /dev/null +++ b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/ElementalType.cs @@ -0,0 +1,101 @@ +using System; + +namespace BetterGenshinImpact.GameTask.AutoGeniusInvokation.Model +{ + public enum ElementalType + { + Omni, + Cryo, + Hydro, + Pyro, + Electro, + Dendro, + Anemo, + Geo + } + + public static class ElementalTypeExtension + { + public static ElementalType ToElementalType(this string type) + { + type = type.ToLower(); + switch (type) + { + case "omni": + return ElementalType.Omni; + case "cryo": + return ElementalType.Cryo; + case "hydro": + return ElementalType.Hydro; + case "pyro": + return ElementalType.Pyro; + case "electro": + return ElementalType.Electro; + case "dendro": + return ElementalType.Dendro; + case "anemo": + return ElementalType.Anemo; + case "geo": + return ElementalType.Geo; + default: + throw new ArgumentOutOfRangeException(nameof(type), type, null); + } + } + + public static ElementalType ChineseToElementalType(this string type) + { + type = type.ToLower(); + switch (type) + { + case "全": + return ElementalType.Omni; + case "冰": + return ElementalType.Cryo; + case "水": + return ElementalType.Hydro; + case "火": + return ElementalType.Pyro; + case "雷": + return ElementalType.Electro; + case "草": + return ElementalType.Dendro; + case "风": + return ElementalType.Anemo; + case "岩": + return ElementalType.Geo; + default: + throw new ArgumentOutOfRangeException(nameof(type), type, null); + } + } + + public static string ToChinese(this ElementalType type) + { + switch (type) + { + case ElementalType.Omni: + return "全"; + case ElementalType.Cryo: + return "冰"; + case ElementalType.Hydro: + return "水"; + case ElementalType.Pyro: + return "火"; + case ElementalType.Electro: + return "雷"; + case ElementalType.Dendro: + return "草"; + case ElementalType.Anemo: + return "风"; + case ElementalType.Geo: + return "岩"; + default: + throw new ArgumentOutOfRangeException(nameof(type), type, null); + } + } + + public static string ToLowerString(this ElementalType type) + { + return type.ToString().ToLower(); + } + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/RollPhaseDice.cs b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/RollPhaseDice.cs new file mode 100644 index 00000000..984f825d --- /dev/null +++ b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/RollPhaseDice.cs @@ -0,0 +1,42 @@ +using System; +using System.Drawing; + +namespace BetterGenshinImpact.GameTask.AutoGeniusInvokation.Model +{ + /// + /// 投掷期间骰子 + /// + [Obsolete] + public class RollPhaseDice + { + /// + /// 元素类型 + /// + public ElementalType Type { get; set; } + + /// + /// 中心点位置 + /// + public Point CenterPosition { get; set; } + + public RollPhaseDice(ElementalType type, Point centerPosition) + { + Type = type; + CenterPosition = centerPosition; + } + + public RollPhaseDice() + { + } + + public override string ToString() + { + return $"Type:{Type},CenterPosition:{CenterPosition}"; + } + + public void Click() + { + //MouseUtils.Click(CenterPosition.X, CenterPosition.Y); + } + } +} diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/RoundStrategy.cs b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/RoundStrategy.cs new file mode 100644 index 00000000..24c4ed44 --- /dev/null +++ b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/RoundStrategy.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; + +namespace BetterGenshinImpact.GameTask.AutoGeniusInvokation.Model; + +[Obsolete] +public class RoundStrategy +{ + public List RawCommandList { get; set; } = new List(); + + public List ActionCommands { get; set; } = new List(); + + public List MaybeNeedElement(Duel duel) + { + List result = new List(); + + for (int i = 0; i < ActionCommands.Count; i++) + { + if (ActionCommands[i].Action == ActionEnum.SwitchLater + && i != ActionCommands.Count-1 + && ActionCommands[i+1].Action == ActionEnum.UseSkill) + { + result.Add(duel.Characters[ActionCommands[i].TargetIndex].Element); + } + } + return result; + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/Skill.cs b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/Skill.cs new file mode 100644 index 00000000..811c83da --- /dev/null +++ b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Model/Skill.cs @@ -0,0 +1,23 @@ +namespace BetterGenshinImpact.GameTask.AutoGeniusInvokation.Model; + +public class Skill +{ + /// + /// 1-4 和数组下标一致,游戏中是从右往左开始数的! + /// + public short Index { get; set; } + public ElementalType Type { get; set; } + /// + /// 消耗指定元素骰子数量 + /// + public int SpecificElementCost { get; set; } + + /// + /// 消耗杂色骰子数量 + /// + public int AnyElementCost { get; set; } = 0; + /// + /// 消耗指定元素骰子数量 + 消耗杂色骰子数量 = 消耗总骰子数量 + /// + public int AllCost { get; set; } +} \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/ScriptParser.cs b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/ScriptParser.cs new file mode 100644 index 00000000..93414741 --- /dev/null +++ b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/ScriptParser.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Windows.Forms; +using BetterGenshinImpact.GameTask.AutoGeniusInvokation.Model; +using Microsoft.Extensions.Logging; + +namespace BetterGenshinImpact.GameTask.AutoGeniusInvokation; + +public class ScriptParser +{ + private static readonly ILogger MyLogger = App.GetLogger(); + public static Duel Parse(string script) + { + var lines = script.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); + var result = new List(); + foreach (var line in lines) + { + string l = line.Trim(); + result.Add(l); + } + + return Parse(result); + } + + public static Duel Parse(List lines) + { + Duel duel = new Duel(); + string stage = ""; + + int i = 0; + try + { + for (i = 0; i < lines.Count; i++) + { + var line = lines[i]; + if (line.Contains(":")) + { + stage = line; + continue; + } + + if (line == "---" || line.StartsWith("//") || string.IsNullOrEmpty(line)) + { + continue; + } + + if (stage == "角色定义:") + { + var character = ParseCharacter(line); + duel.Characters[character.Index] = character; + } + else if (stage == "策略定义:") + { + MyAssert(duel.Characters[3] != null, "角色未定义"); + + string[] actionParts = line.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries); + MyAssert(actionParts.Length == 3, "策略中的行动命令解析错误"); + MyAssert(actionParts[1] == "使用", "策略中的行动命令解析错误"); + + var actionCommand = new ActionCommand(); + var action = actionParts[1].ChineseToActionEnum(); + actionCommand.Action = action; + + int j = 1; + for (j = 1; j <= 3; j++) + { + var character = duel.Characters[j]; + if (character != null && character.Name == actionParts[0]) + { + actionCommand.Character = character; + break; + } + } + + MyAssert(j <= 3, "策略中的行动命令解析错误:角色名称无法从角色定义中匹配到"); + + int skillNum = int.Parse(Regex.Replace(actionParts[2], @"[^0-9]+", "")); + MyAssert(skillNum < 5, "策略中的行动命令解析错误:技能编号错误"); + actionCommand.TargetIndex = skillNum; + duel.ActionCommandQueue.Add(actionCommand); + } + else + { + throw new System.Exception($"未知的定义字段:{stage}"); + } + } + + MyAssert(duel.Characters[3] != null, "角色未定义,请确认策略文本格式是否为UTF-8"); + } + catch (System.Exception ex) + { + MyLogger.LogError($"解析脚本错误,行号:{i + 1},错误信息:{ex}"); + MessageBox.Show($"解析脚本错误,行号:{i + 1},错误信息:{ex}", "策略解析失败", MessageBoxButtons.OK, MessageBoxIcon.Error); + return null; + } + + return duel; + } + + /// + /// 解析示例 + /// 角色1=刻晴|雷{技能3消耗=1雷骰子+2任意,技能2消耗=3雷骰子,技能1消耗=4雷骰子} + /// 角色2=雷神|雷{技能3消耗=1雷骰子+2任意,技能2消耗=3雷骰子,技能1消耗=4雷骰子} + /// 角色3=甘雨|冰{技能4消耗=1冰骰子+2任意,技能3消耗=1冰骰子,技能2消耗=5冰骰子,技能1消耗=3冰骰子} + /// + /// + /// + public static Character ParseCharacter(string line) + { + var character = new Character(); + + var characterAndSkill = line.Split('{'); + + var parts = characterAndSkill[0].Split('='); + character.Index = int.Parse(Regex.Replace(parts[0], @"[^0-9]+", "")); + MyAssert(character.Index >= 1 && character.Index <= 3, "角色序号必须在1-3之间"); + var nameAndElement = parts[1].Split('|'); + character.Name = nameAndElement[0]; + character.Element = nameAndElement[1].Substring(0, 1).ChineseToElementalType(); + + // 技能 + string skillStr = characterAndSkill[1].Replace("}", ""); + var skillParts = skillStr.Split(','); + var skills = new Skill[skillParts.Length + 1]; + for (int i = 0; i < skillParts.Length; i++) + { + var skill = ParseSkill(skillParts[i]); + skills[skill.Index] = skill; + } + + character.Skills = skills.ToArray(); + return character; + } + + /// + /// 技能3消耗=1雷骰子+2任意 + /// 技能2消耗=3雷骰子 + /// 技能1消耗=4雷骰子 + /// + /// + /// + public static Skill ParseSkill(string oneSkillStr) + { + var skill = new Skill(); + var parts = oneSkillStr.Split('='); + skill.Index = short.Parse(Regex.Replace(parts[0], @"[^0-9]+", "")); + MyAssert(skill.Index >= 1 && skill.Index <= 5, "技能序号必须在1-5之间"); + var costStr = parts[1]; + var costParts = costStr.Split('+'); + skill.SpecificElementCost = int.Parse(costParts[0].Substring(0, 1)); + skill.Type = costParts[0].Substring(1, 1).ChineseToElementalType(); + // 杂色骰子在+号右边 + if (costParts.Length > 1) + { + skill.AnyElementCost = int.Parse(costParts[1].Substring(0, 1)); + } + + skill.AllCost = skill.SpecificElementCost + skill.AnyElementCost; + return skill; + } + + private static void MyAssert(bool b, string msg) + { + if (!b) + { + throw new System.Exception(msg); + } + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Utils/Retry.cs b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Utils/Retry.cs new file mode 100644 index 00000000..e29084af --- /dev/null +++ b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Utils/Retry.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace GeniusInvokationAutoToy.Utils +{ + /// + /// https://stackoverflow.com/questions/1563191/cleanest-way-to-write-retry-logic + /// + public static class Retry + { + public static void Do( + Action action, + TimeSpan retryInterval, + int maxAttemptCount = 3) + { + Do(() => + { + action(); + return null; + }, retryInterval, maxAttemptCount); + } + + public static T Do( + Func action, + TimeSpan retryInterval, + int maxAttemptCount = 3) + { + var exceptions = new List(); + + for (int attempted = 0; attempted < maxAttemptCount; attempted++) + { + try + { + if (attempted > 0) + { + Thread.Sleep(retryInterval); + } + + return action(); + } + catch (Exception ex) + { + exceptions.Add(ex); + } + } + + throw new AggregateException(exceptions); + } + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/Model/RectArea.cs b/BetterGenshinImpact/GameTask/Model/RectArea.cs index 204d239a..489c0607 100644 --- a/BetterGenshinImpact/GameTask/Model/RectArea.cs +++ b/BetterGenshinImpact/GameTask/Model/RectArea.cs @@ -193,22 +193,22 @@ public class RectArea : IDisposable return _srcBitmap != null || _srcMat != null; } - /// - /// 在本区域内查找目标图像 - /// - /// - /// - [Obsolete] - public RectArea Find(Mat targetImageMat) - { - if (!HasImage()) - { - throw new Exception("当前对象内没有图像内容,无法完成 Find 操作"); - } + ///// + ///// 在本区域内查找目标图像 + ///// + ///// + ///// + //[Obsolete] + //public RectArea Find(Mat targetImageMat) + //{ + // if (!HasImage()) + // { + // throw new Exception("当前对象内没有图像内容,无法完成 Find 操作"); + // } - var p = OldMatchTemplateHelper.FindSingleTarget(SrcGreyMat, targetImageMat); - return p is { X: > 0, Y: > 0 } ? new RectArea(targetImageMat, p.X - targetImageMat.Width / 2, p.Y - targetImageMat.Height / 2, this) : new RectArea(); - } + // var p = OldMatchTemplateHelper.FindSingleTarget(SrcGreyMat, targetImageMat); + // return p is { X: > 0, Y: > 0 } ? new RectArea(targetImageMat, p.X - targetImageMat.Width / 2, p.Y - targetImageMat.Height / 2, this) : new RectArea(); + //} /// /// 在本区域内查找识别对象 @@ -343,22 +343,22 @@ public class RectArea : IDisposable return ra; } - /// - /// 找到图像并点击中心 - /// - /// - /// - [Obsolete] - public RectArea ClickCenter(Mat targetImageMat) - { - var ra = Find(targetImageMat); - if (!ra.IsEmpty()) - { - ra.ClickCenter(); - } + ///// + ///// 找到图像并点击中心 + ///// + ///// + ///// + //[Obsolete] + //public RectArea ClickCenter(Mat targetImageMat) + //{ + // var ra = Find(targetImageMat); + // if (!ra.IsEmpty()) + // { + // ra.ClickCenter(); + // } - return ra; - } + // return ra; + //} /// /// 当前对象点击中心 diff --git a/BetterGenshinImpact/GameTask/Model/SystemInfo.cs b/BetterGenshinImpact/GameTask/Model/SystemInfo.cs index 87be90bd..73d0c5d6 100644 --- a/BetterGenshinImpact/GameTask/Model/SystemInfo.cs +++ b/BetterGenshinImpact/GameTask/Model/SystemInfo.cs @@ -27,7 +27,7 @@ namespace BetterGenshinImpact.GameTask.Model /// 捕获窗口区域 现在已经和实际游戏画面一致 /// CaptureAreaRect = GameScreenSize or GameWindowRect /// - public RECT CaptureAreaRect { get; } + public RECT CaptureAreaRect { get; set; } public Process GameProcess { get; } diff --git a/BetterGenshinImpact/GameTask/Placeholder/PlaceholderTrigger.cs b/BetterGenshinImpact/GameTask/Placeholder/PlaceholderTrigger.cs index 92b5abd5..44224197 100644 --- a/BetterGenshinImpact/GameTask/Placeholder/PlaceholderTrigger.cs +++ b/BetterGenshinImpact/GameTask/Placeholder/PlaceholderTrigger.cs @@ -14,21 +14,9 @@ public class TestTrigger : ITaskTrigger public int Priority => 9999; public bool IsExclusive { get; private set; } - private readonly RecognitionObject _optionButtonRo; - - private readonly AutoFishingAssets _autoFishingAssets; - public TestTrigger() { var info = TaskContext.Instance().SystemInfo; - _optionButtonRo = new RecognitionObject - { - Name = "OptionButton", - RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssertImage("AutoSkip", "option.png"), - DrawOnWindow = true - }.InitTemplate(); - _autoFishingAssets = new AutoFishingAssets(); } public void Init() diff --git a/BetterGenshinImpact/GameTask/SystemControl.cs b/BetterGenshinImpact/GameTask/SystemControl.cs index aeb53171..a9cf3ce0 100644 --- a/BetterGenshinImpact/GameTask/SystemControl.cs +++ b/BetterGenshinImpact/GameTask/SystemControl.cs @@ -12,8 +12,7 @@ public class SystemControl return FindHandleByProcessName("YuanShen", "GenshinImpact", "Genshin Impact Cloud Game"); } - [Obsolete] - public static bool IsGenshinImpactActiveOld() + public static bool IsGenshinImpactActiveByProcess() { var name = GetActiveProcessName(); return name is "YuanShen" or "GenshinImpact" or "Genshin Impact Cloud Game"; @@ -119,6 +118,15 @@ public class SystemControl User32.SetForegroundWindow(hWnd); } + public static void ActivateWindow() + { + if (!TaskContext.Instance().IsInitialized) + { + throw new Exception("请先启动BetterGI"); + } + ActivateWindow(TaskContext.Instance().GameHandle); + } + public static bool IsFullScreenMode(IntPtr hWnd) { if (hWnd == IntPtr.Zero) diff --git a/BetterGenshinImpact/Helpers/Extensions/ClickExtension.cs b/BetterGenshinImpact/Helpers/Extensions/ClickExtension.cs index 3fa1cc83..c43d2782 100644 --- a/BetterGenshinImpact/Helpers/Extensions/ClickExtension.cs +++ b/BetterGenshinImpact/Helpers/Extensions/ClickExtension.cs @@ -1,5 +1,6 @@ using BetterGenshinImpact.Core.Simulator; using OpenCvSharp; +using WindowsInput; namespace BetterGenshinImpact.Helpers.Extensions; @@ -17,9 +18,20 @@ public static class ClickExtension (rect.Y + rect.Height * 1d / 2) * 65535 / PrimaryScreen.WorkingArea.Height).LeftButtonClick(); } - public static void Click(int x, int y) + public static IMouseSimulator Click(int x, int y) { - Simulation.SendInput.Mouse.MoveMouseTo(x * 65535 * 1d / PrimaryScreen.WorkingArea.Width, + return Simulation.SendInput.Mouse.MoveMouseTo(x * 65535 * 1d / PrimaryScreen.WorkingArea.Width, y * 65535 * 1d / PrimaryScreen.WorkingArea.Height).LeftButtonClick(); } + + public static IMouseSimulator Move(double x, double y) + { + return Simulation.SendInput.Mouse.MoveMouseTo(x * 65535 * 1d / PrimaryScreen.WorkingArea.Width, + y * 65535 * 1d / PrimaryScreen.WorkingArea.Height).LeftButtonClick(); + } + + public static IMouseSimulator Move(Point p) + { + return Move(p.X, p.Y); + } } \ No newline at end of file