diff --git a/BetterGenshinImpact/Core/Script/Project/Manifest.cs b/BetterGenshinImpact/Core/Script/Project/Manifest.cs index ca13e47e..0aa31798 100644 --- a/BetterGenshinImpact/Core/Script/Project/Manifest.cs +++ b/BetterGenshinImpact/Core/Script/Project/Manifest.cs @@ -7,6 +7,8 @@ using BetterGenshinImpact.Core.Config; using BetterGenshinImpact.GameTask.Common; using BetterGenshinImpact.Model; using Microsoft.Extensions.Logging; +using System.Text.Json.Serialization; +using System.Linq; namespace BetterGenshinImpact.Core.Script.Project; @@ -77,4 +79,21 @@ public class Manifest return settingItems; } + + [JsonIgnore] + public string ShortDescription + { + get + { + var lines = this.Description.Split('\n'); + if (lines.Length > 6) + { + return String.Join('\n', lines.Take(6).Append("……")); + } + else + { + return this.Description; + } + } + } } diff --git a/BetterGenshinImpact/GameTask/AutoArtifactSalvage/ArtifactStat.cs b/BetterGenshinImpact/GameTask/AutoArtifactSalvage/ArtifactStat.cs index 214c6887..9a31e297 100644 --- a/BetterGenshinImpact/GameTask/AutoArtifactSalvage/ArtifactStat.cs +++ b/BetterGenshinImpact/GameTask/AutoArtifactSalvage/ArtifactStat.cs @@ -1,3 +1,5 @@ +using System.Text; + namespace BetterGenshinImpact.GameTask.AutoArtifactSalvage { /// @@ -34,5 +36,24 @@ namespace BetterGenshinImpact.GameTask.AutoArtifactSalvage public int Level { get; private set; } // PS:圣遗物的种类和品质在点击查看之前就可以通过识别图标获悉,所以不必在此模型类中获取 + + /// + /// 生成一个手工拼接的成员结构示意字符串 + /// + /// + public string ToStructuredString() + { + StringBuilder sb = new StringBuilder(); + sb.Append("Properties").Append('\n'); + sb.Append("├─").Append("Name: ").Append(this.Name).Append('\n'); + sb.Append("├─").Append("MainAffix: ").Append(this.MainAffix.Type).Append(", ").Append(this.MainAffix.Value).Append('\n'); + sb.Append("├─").Append("MinorAffixes: ").Append('\n'); + for (int i = 0; i < this.MinorAffixes.Length; i++) + { + sb.Append('│').Append('\t').Append(i == this.MinorAffixes.Length - 1 ? "└─" : "├─").Append($"[{i}]: ").Append(this.MinorAffixes[i].Type).Append(", ").Append(this.MinorAffixes[i].Value).Append('\n'); + } + sb.Append("└─").Append("Level: ").Append(this.Level); + return sb.ToString(); + } } } diff --git a/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageConfig.cs b/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageConfig.cs index 28a61138..408e838e 100644 --- a/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageConfig.cs +++ b/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageConfig.cs @@ -8,12 +8,11 @@ public partial class AutoArtifactSalvageConfig : ObservableObject { // JavaScript [ObservableProperty] - private string _javaScript = - @"(async function (artifact) { - var hasATK = Array.from(artifact.MinorAffixes).some(affix => affix.Type == 'ATK'); - var hasDEF = Array.from(artifact.MinorAffixes).some(affix => affix.Type == 'DEF'); - Output = hasATK && hasDEF; - })(ArtifactStat);"; + private string _javaScript = @"(async function (artifact) { + var hasATK = Array.from(artifact.MinorAffixes).some(affix => affix.Type == 'ATK'); + var hasDEF = Array.from(artifact.MinorAffixes).some(affix => affix.Type == 'DEF'); + Output = hasATK && hasDEF; +})(ArtifactStat);"; // 正则表达式 [Obsolete] @@ -28,4 +27,8 @@ public partial class AutoArtifactSalvageConfig : ObservableObject // 最多检查多少个圣遗物 [ObservableProperty] private int _maxNumToCheck = 100; + + // 单次识别失败政策 + [ObservableProperty] + private RecognitionFailurePolicy _recognitionFailurePolicy = RecognitionFailurePolicy.Skip; } \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageTask.cs b/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageTask.cs index f5289b88..92856d00 100644 --- a/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageTask.cs +++ b/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageTask.cs @@ -18,6 +18,7 @@ using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; using OpenCvSharp; using System; +using System.Collections.Frozen; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; @@ -35,7 +36,7 @@ namespace BetterGenshinImpact.GameTask.AutoArtifactSalvage; /// public class AutoArtifactSalvageTask : ISoloTask { - private readonly ILogger logger = App.GetLogger(); + private readonly ILogger logger; private readonly InputSimulator input = Simulation.SendInput; private CancellationToken ct; @@ -52,17 +53,23 @@ public class AutoArtifactSalvageTask : ISoloTask private readonly int? maxNumToCheck; + private readonly RecognitionFailurePolicy? recognitionFailurePolicy; + private readonly bool returnToMainUi = true; - private readonly CultureInfo cultureInfo; + private readonly CultureInfo? cultureInfo; - public AutoArtifactSalvageTask(int star, string? javaScript = null, int? maxNumToCheck = null) + private readonly FrozenDictionary artifactAffixStrDic; + + public AutoArtifactSalvageTask(AutoArtifactSalvageTaskParam param, ILogger? logger = null) { - this.star = star; - this.javaScript = javaScript; - this.maxNumToCheck = maxNumToCheck; - IStringLocalizer stringLocalizer = App.GetService>() ?? throw new NullReferenceException(); - this.cultureInfo = new CultureInfo(TaskContext.Instance().Config.OtherConfig.GameCultureInfoName); + this.star = param.Star; + this.javaScript = param.JavaScript; + this.maxNumToCheck = param.MaxNumToCheck; + this.recognitionFailurePolicy = param.RecognitionFailurePolicy; + this.logger = logger ?? App.GetLogger(); + var stringLocalizer = param.StringLocalizer ?? App.GetService>() ?? throw new NullReferenceException(); + this.cultureInfo = param.GameCultureInfo; quickSelectLocalizedString = stringLocalizer.WithCultureGet(cultureInfo, "快速选择"); numOfStarLocalizedString = [ @@ -71,11 +78,8 @@ public class AutoArtifactSalvageTask : ISoloTask stringLocalizer.WithCultureGet(cultureInfo, "3星圣遗物"), stringLocalizer.WithCultureGet(cultureInfo, "4星圣遗物") ]; - } - public AutoArtifactSalvageTask(int star, bool returnToMainUi) : this(star) - { - this.returnToMainUi = returnToMainUi; + artifactAffixStrDic = ArtifactAffix.DefaultStrDic.Select(kvp => new KeyValuePair(kvp.Key, stringLocalizer.WithCultureGet(cultureInfo, kvp.Value))).ToFrozenDictionary(); } public static async Task OpenBag(GridScreenName gridScreenName, InputSimulator input, ILogger logger, CancellationToken ct) @@ -260,7 +264,7 @@ public class AutoArtifactSalvageTask : ISoloTask // 分解5星 if (javaScript != null) { - await Salvage5Star(this.javaScript, this.maxNumToCheck ?? throw new ArgumentException($"{nameof(this.maxNumToCheck)}不能为空")); + await Salvage5Star(); logger.LogInformation("筛选完毕,请复查并手动分解"); } else @@ -274,9 +278,11 @@ public class AutoArtifactSalvageTask : ISoloTask } } - private async Task Salvage5Star(string javaScript, int maxNumToCheck) + private async Task Salvage5Star() { - int count = maxNumToCheck; + string javaScript = this.javaScript ?? throw new ArgumentException($"{nameof(this.javaScript)}不能为空"); + int count = this.maxNumToCheck ?? throw new ArgumentException($"{nameof(this.maxNumToCheck)}不能为空"); + RecognitionFailurePolicy recognitionFailurePolicy = this.recognitionFailurePolicy ?? throw new ArgumentException($"{nameof(this.recognitionFailurePolicy)}不能为空"); using var ra0 = CaptureToRectArea(); GridScreenParams gridParams = GridScreenParams.Templates[GridScreenName.ArtifactSalvage]; @@ -294,8 +300,28 @@ public class AutoArtifactSalvageTask : ISoloTask using ImageRegion itemRegion1 = ra1.DeriveCrop(gridRect + new Point(gridRoi.X, gridRoi.Y)); if (GetArtifactStatus(itemRegion1.SrcMat) == ArtifactStatus.Selected) { - using ImageRegion card = ra1.DeriveCrop(new Rect((int)(ra1.Width * 0.70), (int)(ra1.Width * 0.055), (int)(ra1.Width * 0.24), (int)(ra1.Width * 0.29))); - ArtifactStat artifact = GetArtifactStat(card.SrcMat, OcrFactory.Paddle, this.cultureInfo, out string allText); + using ImageRegion card = ra1.DeriveCrop(new Rect((int)(ra1.Width * 0.70), (int)(ra1.Height * 0.112), (int)(ra1.Width * 0.275), (int)(ra1.Height * 0.50))); + + ArtifactStat artifact; + try + { + artifact = GetArtifactStat(card.SrcMat, OcrFactory.Paddle, out string allText); + } + catch (Exception e) + { + if (recognitionFailurePolicy == RecognitionFailurePolicy.Skip) + { + logger.LogError("识别失败,跳过当前圣遗物:{msg}", e.Message); + + itemRegion.Click(); // 反选取消 + await Delay(100, ct); + continue; + } + else + { + throw; + } + } if (IsMatchJavaScript(artifact, javaScript)) { @@ -379,35 +405,78 @@ public class AutoArtifactSalvageTask : ISoloTask return match.Success; } - public static ArtifactStat GetArtifactStat(Mat src, IOcrService ocrService, CultureInfo cultureInfo, out string allText) + public ArtifactStat GetArtifactStat(Mat src, IOcrService ocrService, out string allText) { - var ocrResult = ocrService.OcrResult(src); - allText = ocrResult.Text; - var lines = ocrResult.Text.Split('\n'); + using Mat gray = src.CvtColor(ColorConversionCodes.BGR2GRAY); + Mat hatKernel = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(15, 15)/*需根据实际文本大小调整*/); // 顶帽运算核 + + Mat nameRoi = gray.SubMat(new Rect(0, 0, src.Width, (int)(src.Height * 0.106))); + //Cv2.ImShow("name", nameRoi); + Mat typeRoi = gray.SubMat(new Rect(0, (int)(src.Height * 0.106), src.Width, (int)(src.Height * 0.106))); + #region 主词条预处理 去除背景干扰 + Mat mainAffixRoi = gray.SubMat(new Rect(0, (int)(src.Height * 0.22), (int)(src.Width * 0.55), (int)(src.Height * 0.30))); + using Mat mainAffixRoiBottomHat = mainAffixRoi.MorphologyEx(MorphTypes.TopHat, hatKernel); + using Mat mainAffixRoiThreshold = mainAffixRoiBottomHat.Threshold(30, 255, ThresholdTypes.Binary); + //Cv2.ImShow("mainAffix", mainAffixRoiThreshold); + #endregion + #region 副词条预处理 还是不处理效果最好…… + Mat levelAndMinorAffixRoi = gray.SubMat(new Rect(0, (int)(src.Height * 0.52), src.Width, (int)(src.Height * 0.48))); + //using Mat levelAndMinorAffixRoiThreshold = new Mat(); + //double otsu = Cv2.Threshold(levelAndMinorAffixRoi, levelAndMinorAffixRoiThreshold, 0, 255, ThresholdTypes.Binary | ThresholdTypes.Otsu); + // //using Mat levelAndMinorAffixRoiThreshold = levelAndMinorAffixRoi.Threshold(170, 255, ThresholdTypes.Binary); + //Cv2.ImShow($"levelAndMinorAffixRoi = {otsu}", levelAndMinorAffixRoiThreshold); + #endregion + //Cv2.WaitKey(); + + var nameOcrResult = ocrService.OcrResult(nameRoi); + var typeOcrResult = ocrService.OcrResult(typeRoi); + var mainAffixOcrResult = ocrService.OcrResult(mainAffixRoiThreshold); + string mainAffixText = string.Join("\n", mainAffixOcrResult.Regions.Where(r => r.Score > 0.5).OrderBy(r => r.Rect.Center.Y).ThenBy(r => r.Rect.Center.X).Select(r => r.Text)); + var mainAffixLines = mainAffixText.Split('\n'); + var levelAndMinorAffixOcrResult = ocrService.OcrResult(levelAndMinorAffixRoi); + string levelAndMinorAffixText = string.Join("\n", levelAndMinorAffixOcrResult.Regions.Where(r => r.Score > 0.5) + .Where(r => r.Rect.BoundingRect().Left < levelAndMinorAffixRoi.Width * 0.1) // 一定是贴着左边的,排除套装效果文字也存在类似+15%的情况 + .OrderBy(r => r.Rect.Center.Y).ThenBy(r => r.Rect.Center.X).Select(r => r.Text)); + var levelAndMinorAffixLines = levelAndMinorAffixText.Split('\n'); + + allText = String.Join('\n', nameOcrResult.Text, typeOcrResult.Text, mainAffixText, levelAndMinorAffixText); + string percentStr = "%"; // 名称 - string name = lines[0]; + string name = nameOcrResult.Text; #region 主词条 - var defaultMainAffix = ArtifactAffix.DefaultStrDic.Select(kvp => kvp.Value).Distinct(); - string mainAffixTypeLine = lines.Single(l => defaultMainAffix.Contains(l)); - ArtifactAffixType mainAffixType = ArtifactAffix.DefaultStrDic.First(kvp => kvp.Value == mainAffixTypeLine).Key; - string mainAffixValueLine = lines.Select(l => + var defaultMainAffix = this.artifactAffixStrDic.Select(kvp => kvp.Value).Distinct(); + string mainAffixTypeLine = mainAffixLines.SingleOrDefault(l => defaultMainAffix.Contains(l)) ?? throw new Exception($"未找到主词条对应的行:\n{mainAffixText}"); + ArtifactAffixType mainAffixType = this.artifactAffixStrDic.First(kvp => kvp.Value == mainAffixTypeLine).Key; + string mainAffixValueLine = mainAffixLines.Select(l => { - string pattern = @"^(\d+\.?\d*)(%?)$"; + string pattern = @"^([\d., ]*)(%?)$"; pattern = pattern.Replace("%", percentStr); // 这样一行一行写只是为了IDE能保持正则字符串高亮 Match match = Regex.Match(l, pattern); if (match.Success) { + if (mainAffixType == ArtifactAffixType.ATK && !String.IsNullOrEmpty(match.Groups[2].Value)) + { + mainAffixType = ArtifactAffixType.ATKPercent; + } + if (mainAffixType == ArtifactAffixType.DEF && !String.IsNullOrEmpty(match.Groups[2].Value)) + { + mainAffixType = ArtifactAffixType.DEFPercent; + } + if (mainAffixType == ArtifactAffixType.HP && !String.IsNullOrEmpty(match.Groups[2].Value)) + { + mainAffixType = ArtifactAffixType.HPPercent; + } return match.Groups[1].Value; } else { return null; } - }).Where(l => l != null).Cast().Single(); - if (!float.TryParse(mainAffixValueLine, NumberStyles.Any, cultureInfo, out float value)) + }).Where(l => l != null).Cast().SingleOrDefault() ?? throw new Exception($"未找到主词条数值对应的行:\n{mainAffixText}"); + if (!float.TryParse(mainAffixValueLine, NumberStyles.Any, this.cultureInfo, out float value)) { throw new Exception($"未识别的主词条数值:{mainAffixValueLine}"); } @@ -415,15 +484,15 @@ public class AutoArtifactSalvageTask : ISoloTask #endregion #region 副词条 - ArtifactAffix[] minorAffixes = lines.Select(l => + ArtifactAffix[] minorAffixes = levelAndMinorAffixLines.Select(l => { - string pattern = @"^[•·]?([^+]+)\+(\d+\.?\d*)(%?)$"; + string pattern = @"^[•·]?([^+::]+)\+([\d., ]*)(%?)$"; pattern = pattern.Replace("%", percentStr); Match match = Regex.Match(l, pattern); if (match.Success) { ArtifactAffixType artifactAffixType; - var dic = ArtifactAffix.DefaultStrDic; + var dic = this.artifactAffixStrDic; if (match.Groups[1].Value.Contains(dic[ArtifactAffixType.ATK])) { @@ -493,7 +562,7 @@ public class AutoArtifactSalvageTask : ISoloTask #endregion #region 等级 - string levelLine = lines.Select(l => + string levelLine = levelAndMinorAffixLines.Select(l => { string pattern = @"^\+(\d*)$"; Match match = Regex.Match(l, pattern); @@ -505,7 +574,7 @@ public class AutoArtifactSalvageTask : ISoloTask { return null; } - }).Where(l => l != null).Cast().Single(); + }).Where(l => l != null).Cast().SingleOrDefault() ?? throw new Exception($"未找到等级对应的行:\n{levelAndMinorAffixText}"); if (!int.TryParse(levelLine, out int level) || level < 0 || level > 20) { throw new Exception($"未识别的等级:{levelLine}"); diff --git a/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageTask.en.resx b/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageTask.en.resx index 2b727f12..7ee34951 100644 --- a/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageTask.en.resx +++ b/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageTask.en.resx @@ -1,4 +1,4 @@ - +