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 @@
-
+