From f5e22d20cbe92e7580e97846c1aaffafd22d6fd8 Mon Sep 17 00:00:00 2001 From: FishmanTheMurloc <162452111+FishmanTheMurloc@users.noreply.github.com> Date: Fri, 10 Oct 2025 22:09:34 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9C=A3=E9=81=97=E7=89=A9=E5=88=86=E8=A7=A3?= =?UTF-8?q?=E5=A5=97=E8=A3=85=E7=AD=9B=E9=80=89=E7=9A=84=E6=94=B9=E8=BF=9B?= =?UTF-8?q?=E5=92=8CJS=E5=BC=95=E6=93=8E=E7=9A=84=E7=BA=A6=E6=9D=9F=20(#23?= =?UTF-8?q?19)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AutoArtifactSalvageConfig.cs | 11 ++- .../AutoArtifactSalvageTask.cs | 73 +++++++++++++++---- .../Model/GameUI/ArtifactSetFilterScreen.cs | 16 ++-- .../View/Windows/OcrDialog.xaml.cs | 3 +- .../AutoArtifactSalvageTaskTests.cs | 45 ++++++++---- .../GameUI/ArtifactSetFilterScreenTests.cs | 1 + 6 files changed, 111 insertions(+), 38 deletions(-) diff --git a/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageConfig.cs b/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageConfig.cs index 5762ca55..62c083b5 100644 --- a/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageConfig.cs +++ b/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageConfig.cs @@ -8,11 +8,10 @@ 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 = @"var hasATK = Array.from(ArtifactStat.MinorAffixes).some(affix => affix.Type == 'ATK'); +var hasDEF = Array.from(ArtifactStat.MinorAffixes).some(affix => affix.Type == 'DEF'); +var hasHP = Array.from(ArtifactStat.MinorAffixes).some(affix => affix.Type == 'HP'); +Output = (hasATK && hasDEF) || (hasHP && hasDEF);"; // JavaScript [ObservableProperty] @@ -32,7 +31,7 @@ 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 7c61a7ea..894c60e5 100644 --- a/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageTask.cs +++ b/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageTask.cs @@ -12,6 +12,7 @@ using BetterGenshinImpact.GameTask.Model.Area; using BetterGenshinImpact.GameTask.Model.GameUI; using BetterGenshinImpact.Helpers; using BetterGenshinImpact.Helpers.Extensions; +using BetterGenshinImpact.View.Drawable; using Fischless.WindowsInput; using Microsoft.ClearScript; using Microsoft.ClearScript.V8; @@ -303,21 +304,49 @@ public class AutoArtifactSalvageTask : ISoloTask quickSelectConfirmBtn.Click(); await Delay(400, ct); // 点击所属套装 - ra5.ClickTo(315, 205); + ra5.ClickTo(315, 190); await Delay(1000, ct); // 遍历套装Grid勾选套装 using InferenceSession session = GridIconsAccuracyTestTask.LoadModel(out Dictionary prototypes); ArtifactSetFilterScreen gridScreen = new ArtifactSetFilterScreen(new GridParams(new Rect(40, 100, 1300, 852), 2, 3, 40, 40, 0.024), this.logger, this.ct); - await foreach (ImageRegion itemRegion in gridScreen) + string drawKey = "ArtifactSetFilter"; + var drawRectList = new List(); + var drawTextList = new List(); + gridScreen.OnBeforeScroll += () => { VisionContext.Instance().DrawContent.RemoveRect(drawKey); drawRectList.Clear(); drawTextList.Clear(); }; + System.Drawing.Pen greenPen = new System.Drawing.Pen(System.Drawing.Color.Lime); + try { - using Mat img125 = GetGridIconsTask.CropResizeArtifactSetFilterGridIcon(itemRegion); - (string predName, _) = GridIconsAccuracyTestTask.Infer(img125, session, prototypes); - if (this.artifactSetFilter.Contains(predName)) + await foreach (ImageRegion itemRegion in gridScreen) { - itemRegion.Click(); - await Delay(100, ct); + using Mat img125 = GetGridIconsTask.CropResizeArtifactSetFilterGridIcon(itemRegion); + (string? predName, _) = GridIconsAccuracyTestTask.Infer(img125, session, prototypes); + if (predName == null) + { + var rectDrawable = itemRegion.SelfToRectDrawable(drawKey); + drawRectList.Add(rectDrawable); + VisionContext.Instance().DrawContent.PutOrRemoveRectList(drawKey, drawRectList); + drawTextList.Add(new TextDrawable("识别失败", new System.Windows.Point(rectDrawable.Rect.X + rectDrawable.Rect.Width / 3, rectDrawable.Rect.Y))); + VisionContext.Instance().DrawContent.TextList.GetOrAdd(drawKey, drawTextList); + } + else + { + var rectDrawable = itemRegion.SelfToRectDrawable(drawKey, greenPen); + drawRectList.Add(rectDrawable); + VisionContext.Instance().DrawContent.PutOrRemoveRectList(drawKey, drawRectList); + drawTextList.Add(new TextDrawable(predName, new System.Windows.Point(rectDrawable.Rect.X + rectDrawable.Rect.Width / 3, rectDrawable.Rect.Y))); + VisionContext.Instance().DrawContent.TextList.GetOrAdd(drawKey, drawTextList); + if (this.artifactSetFilter.Contains(predName)) + { + itemRegion.Click(); + await Delay(100, ct); + } + } } } + finally + { + VisionContext.Instance().DrawContent.ClearAll(); + } // 点击确认筛选 using var confirmFilterBtnRegion = CaptureToRectArea(); Bv.ClickWhiteConfirmButton(confirmFilterBtnRegion); @@ -386,7 +415,7 @@ public class AutoArtifactSalvageTask : ISoloTask } } - if (IsMatchJavaScript(artifact, javaScript)) + if (await IsMatchJavaScript(artifact, javaScript)) { // logger.LogInformation(message: msg); } @@ -412,20 +441,33 @@ public class AutoArtifactSalvageTask : ISoloTask /// /// 作为JS入参,JS使用“ArtifactStat”获取 /// - /// 由调用者控制生命周期 + /// 为空则默认创建一个3秒延迟的cts /// 是否匹配。取JS的“Output”作为出参 /// /// - public static bool IsMatchJavaScript(ArtifactStat artifact, string javaScript) + public async static Task IsMatchJavaScript(ArtifactStat artifact, string javaScript, ILogger? logger = null, TimeProvider? timeProvider = null) { + logger = logger ?? App.GetLogger(); using V8ScriptEngine engine = new V8ScriptEngine(V8ScriptEngineFlags.UseCaseInsensitiveMemberBinding | V8ScriptEngineFlags.DisableGlobalMembers); + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3), timeProvider ?? TimeProvider.System); // 这里只是用JS写一个自定义判断方法,由于每个圣遗物都会执行一次,这个方法不应执行太久 + cts.Token.Register(() => + { + try + { + engine.Interrupt(); + } + catch (Exception ex) + { + Console.WriteLine($"中断失败: {ex.Message}"); + } + }); try { // 传入输入参数 engine.Script.ArtifactStat = artifact; // 执行JavaScript代码 - engine.Execute(javaScript); + await Task.Run(() => engine.Execute(javaScript)); // 检查是否有输出 if (!engine.Script.propertyIsEnumerable("Output")) @@ -440,6 +482,11 @@ public class AutoArtifactSalvageTask : ISoloTask return (bool)engine.Script.Output; } + catch (ScriptInterruptedException) + { + logger.LogWarning("脚本执行超出3秒限制,请使用正确的JS代码(JavaScript execution timeout!)"); + throw; + } catch (ScriptEngineException ex) { throw new Exception($"JavaScript execution error: {ex.Message}", ex); @@ -560,7 +607,7 @@ public class AutoArtifactSalvageTask : ISoloTask if (!match.Success) { continue; - } + } ArtifactAffixType artifactAffixType; var dic = this.artifactAffixStrDic; if (match.Groups[1].Value.Contains(dic[ArtifactAffixType.ATK])) @@ -621,7 +668,7 @@ public class AutoArtifactSalvageTask : ISoloTask { throw new Exception($"未识别的副词条数值:{match.Groups[2].Value}"); } - + bool isUnactivated = false; // 只有在已经成功识别至少 3 个词条后才执行额外的直方图分析。 if (minorAffixes.Count >= 3) diff --git a/BetterGenshinImpact/GameTask/Model/GameUI/ArtifactSetFilterScreen.cs b/BetterGenshinImpact/GameTask/Model/GameUI/ArtifactSetFilterScreen.cs index 7a664bf0..5430bae0 100644 --- a/BetterGenshinImpact/GameTask/Model/GameUI/ArtifactSetFilterScreen.cs +++ b/BetterGenshinImpact/GameTask/Model/GameUI/ArtifactSetFilterScreen.cs @@ -19,6 +19,7 @@ namespace BetterGenshinImpact.GameTask.Model.GameUI private readonly CancellationToken ct; private readonly ILogger logger; private readonly InputSimulator input = Simulation.SendInput; + internal Action? OnBeforeScroll { get; set; } /// /// 对圣遗物套装筛选界面的操作封装类 @@ -37,11 +38,12 @@ namespace BetterGenshinImpact.GameTask.Model.GameUI } public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) { - return new GridEnumerator(@params.Roi, @params.Columns, new GridScroller(@params, logger, input, ct), ct); + return new GridEnumerator(this, @params.Roi, @params.Columns, new GridScroller(@params, logger, input, ct), ct); } public class GridEnumerator : IAsyncEnumerator { + private readonly ArtifactSetFilterScreen owner; private readonly Rect roi; private readonly CancellationToken ct; private readonly int columns; @@ -51,8 +53,9 @@ namespace BetterGenshinImpact.GameTask.Model.GameUI private ImageRegion? current; ImageRegion IAsyncEnumerator.Current => current ?? throw new NullReferenceException(); - internal GridEnumerator(Rect roi, int columns, GridScroller gridScroller, CancellationToken ct) + internal GridEnumerator(ArtifactSetFilterScreen owner, Rect roi, int columns, GridScroller gridScroller, CancellationToken ct) { + this.owner = owner; this.roi = roi; this.ct = ct; this.columns = columns; @@ -71,6 +74,7 @@ namespace BetterGenshinImpact.GameTask.Model.GameUI ra4.MoveTo(this.roi.X + this.roi.Width / 2, this.roi.Y + this.roi.Height / 2); await TaskControl.Delay(300, ct); + owner.OnBeforeScroll?.Invoke(); if (!await this.gridScroller.TryVerticalScollDown(GetGridItems)) { return false; @@ -205,7 +209,7 @@ namespace BetterGenshinImpact.GameTask.Model.GameUI /// /// 具有行号列号的单元格 /// ColNum和RowNum也是0-based的 - /// 不仅方便编程,ClusterColsAndRows方法也需要一个引用类型 + /// 不仅方便编程,ClusterToCells方法也需要一个引用类型 /// /// private class Cell(Rect rect) @@ -229,11 +233,12 @@ namespace BetterGenshinImpact.GameTask.Model.GameUI var orderByX = result.OrderBy(t => t.Rect.Left).ToArray(); int col = 0; int? lastX = null; + int avgWidth = (int)rects.Average(r => r.Width); for (int i = 0; i < orderByX.Length; i++) { if (lastX != null && orderByX[i].Rect.X - lastX > threshold) { - col++; + col += (int)Math.Round((float)(orderByX[i].Rect.X - lastX.Value) / (avgWidth + threshold)); } orderByX[i].ColNum = col; lastX = orderByX[i].Rect.X; @@ -242,11 +247,12 @@ namespace BetterGenshinImpact.GameTask.Model.GameUI var orderByY = result.OrderBy(t => t.Rect.Top).ToArray(); int row = 0; int? lastY = null; + int avgHeight = (int)rects.Average(r => r.Height); for (int i = 0; i < orderByY.Length; i++) { if (lastY != null && orderByY[i].Rect.Y - lastY > threshold) { - row++; + row += (int)Math.Round((float)(orderByY[i].Rect.Y - lastY.Value) / (avgHeight + threshold)); // 估算隔了多少行 } orderByY[i].RowNum = row; lastY = orderByY[i].Rect.Y; diff --git a/BetterGenshinImpact/View/Windows/OcrDialog.xaml.cs b/BetterGenshinImpact/View/Windows/OcrDialog.xaml.cs index 8b32bc18..bc50d059 100644 --- a/BetterGenshinImpact/View/Windows/OcrDialog.xaml.cs +++ b/BetterGenshinImpact/View/Windows/OcrDialog.xaml.cs @@ -7,6 +7,7 @@ using OpenCvSharp; using System; using System.Globalization; using System.IO; +using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; @@ -53,7 +54,7 @@ public partial class OcrDialog this.ModelStructure.Text = artifact.ToStructuredString(); if (this.javaScript != null) { - bool isMatch = AutoArtifactSalvageTask.IsMatchJavaScript(artifact, this.javaScript); + bool isMatch = Task.Run(() => AutoArtifactSalvageTask.IsMatchJavaScript(artifact, this.javaScript)).Result; this.RegexResult.Text = isMatch ? "匹配" : "不匹配"; } } diff --git a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoArtifactSalvageTests/AutoArtifactSalvageTaskTests.cs b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoArtifactSalvageTests/AutoArtifactSalvageTaskTests.cs index ae11fd79..1259b259 100644 --- a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoArtifactSalvageTests/AutoArtifactSalvageTaskTests.cs +++ b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoArtifactSalvageTests/AutoArtifactSalvageTaskTests.cs @@ -1,7 +1,9 @@ using BetterGenshinImpact.GameTask.AutoArtifactSalvage; using BetterGenshinImpact.GameTask.Model.GameUI; using BetterGenshinImpact.UnitTest.CoreTests.RecognitionTests.OCRTests; +using Microsoft.ClearScript; using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Time.Testing; using OpenCvSharp; using System; using System.Collections.Concurrent; @@ -219,18 +221,16 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoArtifactSalvageTests } [Theory] - [InlineData(@"ArtifactAffixes.png", @"(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);", false)] - [InlineData(@"ArtifactAffixes.png", @"(async function (artifact) { - var level = artifact.Level; - var hasATKPercent = Array.from(artifact.MinorAffixes).some(affix => affix.Type == 'ATKPercent'); - var hasDEF = Array.from(artifact.MinorAffixes).some(affix => affix.Type == 'DEF'); - Output = level == 0 && hasATKPercent && hasDEF; - })(ArtifactStat);", true)] - public void IsMatchJavaScript_JSShouldBeRight(string screenshot, string js, bool expected) + [InlineData(@"ArtifactAffixes.png", @" + var hasATK = Array.from(ArtifactStat.MinorAffixes).some(affix => affix.Type == 'ATK'); + var hasDEF = Array.from(ArtifactStat.MinorAffixes).some(affix => affix.Type == 'DEF'); + Output = hasATK && hasDEF;", false)] + [InlineData(@"ArtifactAffixes.png", @" + var level = ArtifactStat.Level; + var hasATKPercent = Array.from(ArtifactStat.MinorAffixes).some(affix => affix.Type == 'ATKPercent'); + var hasDEF = Array.from(ArtifactStat.MinorAffixes).some(affix => affix.Type == 'DEF'); + Output = level == 0 && hasATKPercent && hasDEF;", true)] + public async Task IsMatchJavaScript_JSShouldBeRight(string screenshot, string js, bool expected) { // using Mat mat = new Mat(@$"..\..\..\Assets\AutoArtifactSalvage\{screenshot}"); @@ -239,10 +239,29 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoArtifactSalvageTests // AutoArtifactSalvageTask sut = new AutoArtifactSalvageTask(new AutoArtifactSalvageTaskParam(5, null, null, null, null, cultureInfo, this.stringLocalizer), new FakeLogger()); ArtifactStat artifact = sut.GetArtifactStat(mat, paddle.Get(), out string _); - bool result = IsMatchJavaScript(artifact, js); + bool result = await IsMatchJavaScript(artifact, js, new FakeLogger()); // Assert.Equal(expected, result); } + + /// + /// 测试JavaScript运行超时的情况,应抛出正确的异常 + /// + /// + [Fact] + public async Task IsMatchJavaScript_Timeout_ShouldThrowException() + { + // + string js = @"while (true) {};"; + FakeTimeProvider timeProvider = new FakeTimeProvider(); + + // + Task sut = IsMatchJavaScript(new ArtifactStat("", new ArtifactAffix(ArtifactAffixType.ATK, 0), [], 0), js, new FakeLogger(), timeProvider); + timeProvider.Advance(TimeSpan.FromSeconds(3)); + + // + await Assert.ThrowsAsync(()=> sut); + } } } diff --git a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/Model/GameUI/ArtifactSetFilterScreenTests.cs b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/Model/GameUI/ArtifactSetFilterScreenTests.cs index b6ff9b3a..1fa9dc30 100644 --- a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/Model/GameUI/ArtifactSetFilterScreenTests.cs +++ b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/Model/GameUI/ArtifactSetFilterScreenTests.cs @@ -13,6 +13,7 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.Model.GameUI [Theory] [InlineData(@"GameUI\ArtifactSetFilterBright.png", 20, 2)] [InlineData(@"GameUI\ArtifactSetFilterDark.png", 20, 2)] + [InlineData(@"GameUI\ArtifactSetFilterBlack.png", 20, 2)] // 只能识别到较少item(12个)的一个特例,用于验证Cell聚簇算法补齐效果 /// /// 测试获取圣遗物套装筛选界面中的项目,结果应正确 ///