diff --git a/BetterGenshinImpact/BetterGenshinImpact.csproj b/BetterGenshinImpact/BetterGenshinImpact.csproj index 4f824e0d..870fd52e 100644 --- a/BetterGenshinImpact/BetterGenshinImpact.csproj +++ b/BetterGenshinImpact/BetterGenshinImpact.csproj @@ -2,7 +2,7 @@ BetterGI - 0.49.1-alpha.3 + 0.50.1-alpha.3 false WinExe net8.0-windows10.0.22621.0 @@ -43,9 +43,9 @@ - - - + + + diff --git a/BetterGenshinImpact/Core/Script/CancellationContext.cs b/BetterGenshinImpact/Core/Script/CancellationContext.cs index 8f7b1986..782e7f57 100644 --- a/BetterGenshinImpact/Core/Script/CancellationContext.cs +++ b/BetterGenshinImpact/Core/Script/CancellationContext.cs @@ -6,15 +6,26 @@ namespace BetterGenshinImpact.Core.Script; public class CancellationContext : Singleton { public CancellationTokenSource Cts { get; set; } = new(); + public bool IsManualStop { get; private set; } private bool disposed; public void Set() { Cts = new CancellationTokenSource(); + IsManualStop = false; disposed = false; } + public void ManualCancel() + { + if (!disposed) + { + IsManualStop = true; + Cts.Cancel(); + } + } + public void Cancel() { if (!disposed) diff --git a/BetterGenshinImpact/Core/Script/Dependence/Dispatcher.cs b/BetterGenshinImpact/Core/Script/Dependence/Dispatcher.cs index 539d24cc..85733596 100644 --- a/BetterGenshinImpact/Core/Script/Dependence/Dispatcher.cs +++ b/BetterGenshinImpact/Core/Script/Dependence/Dispatcher.cs @@ -260,8 +260,8 @@ public class Dispatcher throw new NullReferenceException($"{nameof(soloTask.Config)}为空"); } GridScreenName gridScreenName = ScriptObjectConverter.GetValue((ScriptObject)soloTask.Config, "gridScreenName", null) ?? throw new Exception("gridScreenName为空或错误"); - string foodName = ScriptObjectConverter.GetValue((ScriptObject)soloTask.Config, "foodName", null) ?? throw new Exception("foodName为空"); - return await new CountInventoryItem(gridScreenName, foodName).Start(cancellationToken); + string itemName = ScriptObjectConverter.GetValue((ScriptObject)soloTask.Config, "itemName", null) ?? throw new Exception("itemName为空"); + return await new CountInventoryItem(gridScreenName, itemName).Start(cancellationToken); } default: throw new ArgumentException($"未知的任务名称: {soloTask.Name}", nameof(soloTask.Name)); diff --git a/BetterGenshinImpact/Core/Script/ScriptRepoUpdater.cs b/BetterGenshinImpact/Core/Script/ScriptRepoUpdater.cs index 77c2d48c..fb2ee9f7 100644 --- a/BetterGenshinImpact/Core/Script/ScriptRepoUpdater.cs +++ b/BetterGenshinImpact/Core/Script/ScriptRepoUpdater.cs @@ -113,6 +113,7 @@ public class ScriptRepoUpdater : Singleton await Task.Run(() => { + Repository? repo = null; try { GlobalSettings.SetOwnerValidation(false); @@ -151,7 +152,7 @@ public class ScriptRepoUpdater : Singleton return; } - using var repo = new Repository(repoPath); + repo = new Repository(repoPath); // 检查远程URL是否需要更新 var origin = repo.Network.Remotes["origin"]; @@ -159,6 +160,7 @@ public class ScriptRepoUpdater : Singleton { // 远程URL已更改,需要删除重新克隆 _logger.LogInformation($"远程URL已更改: 从 {origin.Url} 到 {repoUrl},将重新克隆"); + repo?.Dispose(); CloneRepository(repoUrl, repoPath, "release", onCheckoutProgress); updated = true; return; @@ -170,7 +172,8 @@ public class ScriptRepoUpdater : Singleton var fetchOptions = new FetchOptions { - ProxyOptions = { ProxyType = ProxyType.None } + ProxyOptions = { ProxyType = ProxyType.None }, + Depth = 1 // 浅拉取,只获取最新的提交 }; Commands.Fetch(repo, remote.Name, refSpecs, fetchOptions, "拉取最新更新"); @@ -198,6 +201,7 @@ public class ScriptRepoUpdater : Singleton _logger.LogInformation($"检测到远程更新: 本地 {currentCommitSha[..7]} -> 远程 {remoteCommitSha[..7]},将重新克隆"); // commit不一致,删除本地仓库重新克隆 + repo?.Dispose(); CloneRepository(repoUrl, repoPath, "release", onCheckoutProgress); updated = true; } @@ -207,54 +211,48 @@ public class ScriptRepoUpdater : Singleton { _logger.LogError(ex, "Git仓库更新失败"); UIDispatcherHelper.Invoke(() => Toast.Error("脚本仓库更新异常,直接删除后重新克隆\n原因:" + ex.Message)); + repo?.Dispose(); CloneRepository(repoUrl, repoPath, "release", onCheckoutProgress); } + finally + { + repo?.Dispose(); + } }); - // 如果仓库有更新,则标记新repo.json中的更新节点 - if (updated) + // 标记新repo.json中的更新节点 + try { - try + var newRepoJsonPath = Directory.GetFiles(CenterRepoPath, "repo.json", SearchOption.AllDirectories) + .FirstOrDefault(); + if (newRepoJsonPath != null) { - var newRepoJsonPath = Directory.GetFiles(CenterRepoPath, "repo.json", SearchOption.AllDirectories) - .FirstOrDefault(); - if (newRepoJsonPath != null) + var newRepoJsonContent = await File.ReadAllTextAsync(newRepoJsonPath); + + // 检查是否存在repo_update.json,如果存在则直接与它比对 + var repoUpdateJsonPath = Path.Combine(ReposPath, "repo_updated.json"); + string updatedContent; + + if (File.Exists(repoUpdateJsonPath)) { - var newRepoJsonContent = await File.ReadAllTextAsync(newRepoJsonPath); - - // 检查是否存在repo_update.json,如果存在则直接与它比对 - var parentDir = Path.GetDirectoryName(repoPath); - var repoUpdateJsonPath = Path.Combine(parentDir!, "repo_update.json"); - string updatedContent; - - if (File.Exists(repoUpdateJsonPath)) - { - try - { - var repoUpdateContent = await File.ReadAllTextAsync(repoUpdateJsonPath); - updatedContent = AddUpdateMarkersToNewRepo(repoUpdateContent, newRepoJsonContent); - } - catch (Exception ex) - { - updatedContent = AddUpdateMarkersToNewRepo(oldRepoJsonContent ?? "", newRepoJsonContent); - } - } - else - { - // 如果没有repo_update.json,则使用备份的旧内容进行比对 - updatedContent = AddUpdateMarkersToNewRepo(oldRepoJsonContent ?? "", newRepoJsonContent); - } - - // 保存到同级目录 - var updatedRepoJsonPath = Path.Combine(parentDir!, "repo_updated.json"); - await File.WriteAllTextAsync(updatedRepoJsonPath, updatedContent); - _logger.LogInformation($"已标记repo.json中的更新节点并保存到: {updatedRepoJsonPath}"); + var repoUpdateContent = await File.ReadAllTextAsync(repoUpdateJsonPath); + updatedContent = AddUpdateMarkersToNewRepo(repoUpdateContent, newRepoJsonContent); } + else + { + // 如果没有repo_update.json,则使用备份的旧内容进行比对 + updatedContent = AddUpdateMarkersToNewRepo(oldRepoJsonContent ?? "", newRepoJsonContent); + } + + // 保存到同级目录 + var updatedRepoJsonPath = Path.Combine(ReposPath, "repo_updated.json"); + await File.WriteAllTextAsync(updatedRepoJsonPath, updatedContent); + _logger.LogInformation($"已标记repo.json中的更新节点并保存到: {updatedRepoJsonPath}"); } - catch (Exception ex) - { - _logger.LogWarning(ex, "标记repo.json更新节点失败"); - } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "标记repo.json更新节点失败"); } return (repoPath, updated); @@ -314,6 +312,13 @@ public class ScriptRepoUpdater : Singleton // 检查节点本身是否有更新 if (oldNode != null) { + // 若历史上已标记,则保留该标记 + if (IsTruthy(oldNode["hasUpdate"]) || IsTruthy(oldNode["hasUpdated"])) + { + newNode["hasUpdate"] = true; + hasDirectUpdate = true; + } + // 对比时间戳 var oldTime = ParseLastUpdated(oldNode["lastUpdated"]?.ToString()); var newTime = ParseLastUpdated(newNode["lastUpdated"]?.ToString()); @@ -348,10 +353,18 @@ public class ScriptRepoUpdater : Singleton // 如果是叶子节点更新,父节点也标记更新 var isLeafChild = newChildObj["children"] == null || !((JArray?)newChildObj["children"])?.Any() == true; - if (isLeafChild && !hasDirectUpdate && newChildObj["hasUpdate"] != null) + if (isLeafChild && (IsTruthy(newChildObj["hasUpdate"]) || IsTruthy(newChildObj["hasUpdated"]))) { + var parentTime = ParseLastUpdated(newNode["lastUpdated"]?.ToString()); + var childTime = ParseLastUpdated(newChildObj["lastUpdated"]?.ToString()); + newNode["hasUpdate"] = true; hasDirectUpdate = true; + + if (childTime > parentTime && newChildObj["lastUpdated"] != null) + { + newNode["lastUpdated"] = newChildObj["lastUpdated"]; + } } } } @@ -385,6 +398,14 @@ public class ScriptRepoUpdater : Singleton } } + private bool IsTruthy(JToken? token) + { + if (token == null || token.Type == JTokenType.Null) return false; + if (token.Type == JTokenType.Boolean) return (bool)token; + if (token.Type == JTokenType.String) return string.Equals((string)token, "true", StringComparison.OrdinalIgnoreCase); + return false; + } + private static void SimpleCloneRepository(string repoUrl, string repoPath, CheckoutProgressHandler? onCheckoutProgress) { @@ -417,32 +438,41 @@ public class ScriptRepoUpdater : Singleton Directory.CreateDirectory(repoPath); Repository.Init(repoPath); - using var repo = new Repository(repoPath); - GitConfig(repo); + var repo = new Repository(repoPath); - // 添加远程源 - Remote remote = repo.Network.Remotes.Add("origin", repoUrl); - - // 只拉取指定分支 - var fetchOptions = new FetchOptions + try { - TagFetchMode = TagFetchMode.None, - ProxyOptions = { ProxyType = ProxyType.None } - }; - string refSpec = $"+refs/heads/{branchName}:refs/remotes/origin/{branchName}"; - Commands.Fetch(repo, remote.Name, new[] { refSpec }, fetchOptions, "初始化拉取"); + GitConfig(repo); - // 获取远程分支 - var remoteBranch = repo.Branches[$"refs/remotes/origin/{branchName}"]; - if (remoteBranch == null) - throw new Exception($"远程仓库中未找到 {branchName} 分支"); + // 添加远程源 + Remote remote = repo.Network.Remotes.Add("origin", repoUrl); - // 创建并检出本地分支 - var localBranch = repo.CreateBranch(branchName, remoteBranch.Tip); - repo.Branches.Update(localBranch, b => b.TrackedBranch = remoteBranch.CanonicalName); + // 只拉取指定分支 + var fetchOptions = new FetchOptions + { + TagFetchMode = TagFetchMode.None, + ProxyOptions = { ProxyType = ProxyType.None }, + Depth = 1 // 浅拉取,只获取最新的提交 + }; + string refSpec = $"+refs/heads/{branchName}:refs/remotes/origin/{branchName}"; + Commands.Fetch(repo, remote.Name, new[] { refSpec }, fetchOptions, "初始化拉取"); - var checkoutOptions = new CheckoutOptions { OnCheckoutProgress = onCheckoutProgress }; - Commands.Checkout(repo, localBranch, checkoutOptions); + // 获取远程分支 + var remoteBranch = repo.Branches[$"refs/remotes/origin/{branchName}"]; + if (remoteBranch == null) + throw new Exception($"远程仓库中未找到 {branchName} 分支"); + + // 创建并检出本地分支 + var localBranch = repo.CreateBranch(branchName, remoteBranch.Tip); + repo.Branches.Update(localBranch, b => b.TrackedBranch = remoteBranch.CanonicalName); + + var checkoutOptions = new CheckoutOptions { OnCheckoutProgress = onCheckoutProgress }; + Commands.Checkout(repo, localBranch, checkoutOptions); + } + finally + { + repo?.Dispose(); + } } private void GitConfig(Repository repo) diff --git a/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageTask.cs b/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageTask.cs index 5207c3f0..e8b72116 100644 --- a/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageTask.cs +++ b/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageTask.cs @@ -73,7 +73,7 @@ public class AutoArtifactSalvageTask : ISoloTask this.maxNumToCheck = param.MaxNumToCheck; this.recognitionFailurePolicy = param.RecognitionFailurePolicy; this.logger = logger ?? App.GetLogger(); - var stringLocalizer = param.StringLocalizer ?? App.GetService>() ?? throw new NullReferenceException(); + var stringLocalizer = param.StringLocalizer; this.cultureInfo = param.GameCultureInfo; quickSelectLocalizedString = stringLocalizer.WithCultureGet(cultureInfo, "快速选择"); numOfStarLocalizedString = @@ -136,11 +136,21 @@ public class AutoArtifactSalvageTask : ISoloTask // B键打开背包 input.SimulateAction(GIActions.OpenInventory); - await Delay(1000, ct); + await Delay(1200, ct); var openBagSuccess = await NewRetry.WaitForAction(() => { using var ra = CaptureToRectArea(); + + // 判断是否在提示对话框(物品过期提示) + if (Bv.IsInPromptDialog(ra)) + { + // 如果存在物品过期提示,则点击确认按钮 + Bv.ClickWhiteConfirmButton(ra.DeriveCrop(0, 0, ra.Width, ra.Height - ra.Height / 0.2)); + Sleep(300, ct); + return false; + } + using var artifactBtn = ra.Find(recognitionObjectChecked); if (artifactBtn.IsEmpty()) { @@ -203,15 +213,22 @@ public class AutoArtifactSalvageTask : ISoloTask // 快速选择 using var ra3 = CaptureToRectArea(); var ocrList = ra3.FindMulti(RecognitionObject.Ocr(ra3.ToRect().CutLeftBottom(0.25, 0.1))); + bool quickSelectBtnFound = false; foreach (var ocr in ocrList) { if (Regex.IsMatch(ocr.Text, quickSelectLocalizedString)) { + quickSelectBtnFound = true; ocr.Click(); await Delay(500, ct); break; } } + if (!quickSelectBtnFound) + { + logger.LogError("没有找到可匹配{regex}的按钮,终止分解", quickSelectLocalizedString); + return; + } // 确认选择 // 5.5 变成反选 @@ -221,15 +238,22 @@ public class AutoArtifactSalvageTask : ISoloTask List ocrList2 = ra4.FindMulti(RecognitionObject.Ocr(ra4.ToRect().CutLeft(0.20))); for (int i = star; i < 4; i++) { + bool numOfStarFound = false; foreach (var ocr in ocrList2) { if (Regex.IsMatch(ocr.Text, numOfStarLocalizedString[i])) { + numOfStarFound = true; ocr.Click(); await Delay(500, ct); break; } } + if (!numOfStarFound) + { + logger.LogError("没有找到可匹配{regex}的按钮,终止分解", numOfStarLocalizedString[i]); + return; + } } } @@ -525,7 +549,7 @@ public class AutoArtifactSalvageTask : ISoloTask #region 副词条 ArtifactAffix[] minorAffixes = levelAndMinorAffixLines.Select(l => { - string pattern = @"^[•·]?([^+::]+)\+([\d., ]*)(%?)$"; + string pattern = @"^([^+::]+)\+([\d., ]*)(%?).*$"; pattern = pattern.Replace("%", percentStr); Match match = Regex.Match(l, pattern); if (match.Success) diff --git a/BetterGenshinImpact/GameTask/AutoDomain/AutoDomainTask.cs b/BetterGenshinImpact/GameTask/AutoDomain/AutoDomainTask.cs index 389e8c1c..293a606b 100644 --- a/BetterGenshinImpact/GameTask/AutoDomain/AutoDomainTask.cs +++ b/BetterGenshinImpact/GameTask/AutoDomain/AutoDomainTask.cs @@ -67,6 +67,7 @@ public class AutoDomainTask : ISoloTask private readonly string matchingChallengeString; private readonly string rapidformationString; private readonly string limitedFullyString; + private readonly string limitedFullyAllString; private List _resinPriorityListWhenSpecifyUse; @@ -93,6 +94,7 @@ public class AutoDomainTask : ISoloTask this.matchingChallengeString = stringLocalizer.WithCultureGet(cultureInfo, "匹配挑战"); this.rapidformationString = stringLocalizer.WithCultureGet(cultureInfo, "快速编队"); this.limitedFullyString = stringLocalizer.WithCultureGet(cultureInfo, "限时全开"); + this.limitedFullyAllString = stringLocalizer.WithCultureGet(cultureInfo, "限时开放"); } private static RecognitionObject GetConfirmRa(params string[] targetText) @@ -381,7 +383,7 @@ public class AutoDomainTask : ISoloTask limitedFullyStringRa.FindMulti(RecognitionObject.Ocr(0, 0, limitedFullyStringRa.Width * 0.5, limitedFullyStringRa.Height)); var limitedFullyStringRaocrListdone = limitedFullyStringRaocrList.LastOrDefault(t => - Regex.IsMatch(t.Text, this.limitedFullyString)); + Regex.IsMatch(t.Text, this.limitedFullyString) || Regex.IsMatch(t.Text, this.limitedFullyAllString)); // 检测是否为限时全开秘境 if (limitedFullyStringRaocrListdone != null) { @@ -1102,7 +1104,7 @@ public class AutoDomainTask : ISoloTask { // 自动刷干树脂 // 识别树脂状况 - var resinStatus = ResinStatus.RecogniseFromRegion(ra3); + var resinStatus = ResinStatus.RecogniseFromRegion(ra3, TaskContext.Instance().SystemInfo, OcrFactory.Paddle); resinStatus.Print(Logger); if (resinStatus is { CondensedResinCount: <= 0, OriginalResinCount: < 20 }) diff --git a/BetterGenshinImpact/GameTask/AutoDomain/Model/ResinStatus.cs b/BetterGenshinImpact/GameTask/AutoDomain/Model/ResinStatus.cs index 2b292f94..54bf7bea 100644 --- a/BetterGenshinImpact/GameTask/AutoDomain/Model/ResinStatus.cs +++ b/BetterGenshinImpact/GameTask/AutoDomain/Model/ResinStatus.cs @@ -1,10 +1,11 @@ -using System; +using BetterGenshinImpact.Core.Recognition; using BetterGenshinImpact.Core.Recognition.OCR; -using BetterGenshinImpact.GameTask.AutoFight.Assets; +using BetterGenshinImpact.GameTask.Model; using BetterGenshinImpact.GameTask.Model.Area; using BetterGenshinImpact.Helpers; using Microsoft.Extensions.Logging; using OpenCvSharp; +using System; namespace BetterGenshinImpact.GameTask.AutoDomain.Model; @@ -18,35 +19,43 @@ public class ResinStatus /// /// 脆弱树脂(60) /// - public int FragileResinCount { get; set; } = 0; + public int FragileResinCount { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } /// - /// 浓缩树脂(40) + /// 浓缩树脂(60) /// public int CondensedResinCount { get; set; } = 0; /// /// 须臾树脂(60壶内购买) /// - public int TransientResinCount { get; set; } = 0; + public int TransientResinCount { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - public static ResinStatus RecogniseFromRegion(ImageRegion region) + public static ResinStatus RecogniseFromRegion(ImageRegion region, ISystemInfo systemInfo, IOcrService ocrService) { var status = new ResinStatus(); - // 1. 原粹树脂 起点 w-(256+100) ~ w-256 - var captureArea = TaskContext.Instance().SystemInfo.ScaleMax1080PCaptureRect; - var assetScale = TaskContext.Instance().SystemInfo.AssetScale; - var originalResinTopIconRa = AutoFightAssets.Instance.OriginalResinTopIconRa; - var originalResinRes = region.Find(originalResinTopIconRa); + // 1. 原粹树脂 + var assetScale = systemInfo.AssetScale; + var originalResinTopIconRa = new RecognitionObject + { + Name = "OriginalResinTopIcon", + RecognitionType = RecognitionTypes.TemplateMatch, + TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "original_resin_top_icon.png", systemInfo), + DrawOnWindow = false + }.InitTemplate(); + using ImageRegion crop1 = region.DeriveCrop(new Rect((int)(1300 * assetScale), (int)(25 * assetScale), (int)(160 * assetScale), (int)(50 * assetScale))); // 数字位数的不同导致了水平方向上宽泛的区域 + //Cv2.ImShow("test", crop1.SrcMat); + //Cv2.WaitKey(); + var originalResinRes = crop1.Find(originalResinTopIconRa); if (originalResinRes.IsEmpty()) { throw new Exception("未找到原粹树脂图标"); } - var originalResinCountRect = new Rect(originalResinRes.Right + 30, (int)(37 * assetScale), - captureArea.Width - (originalResinRes.Right + 30) - (int)(190 * assetScale), (int)(21 * assetScale)); - string cnt1 = OcrFactory.Paddle.OcrWithoutDetector(region.DeriveCrop(originalResinCountRect).SrcMat); + var originalResinCountRect = new Rect(crop1.X + originalResinRes.Right + (int)(25 * assetScale), (int)(37 * assetScale), (int)(110 * assetScale)/* 考虑最长的“200/200” */, (int)(24 * assetScale)); + using ImageRegion originalResinCountRegion = region.DeriveCrop(originalResinCountRect); + string cnt1 = ocrService.OcrWithoutDetector(originalResinCountRegion.SrcMat); var match = System.Text.RegularExpressions.Regex.Match(cnt1, @"(\d+)\s*[/17]\s*(2|20|200)"); if (match.Success) { @@ -55,12 +64,27 @@ public class ResinStatus } // 2. 浓缩树脂 - var condensedResinRes = region.Find(AutoFightAssets.Instance.CondensedResinTopIconRa); + int startX = crop1.X + originalResinRes.Left - (int)(180 * assetScale); // 从原粹树脂图标位置起算 + var condensedResinTopIconRa = new RecognitionObject + { + Name = "CondensedResinTopIcon", + RecognitionType = RecognitionTypes.TemplateMatch, + TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "condensed_resin_top_icon.png", systemInfo), + DrawOnWindow = false + }.InitTemplate(); + using ImageRegion crop2 = region.DeriveCrop(new Rect(startX, (int)(25 * assetScale), (int)(90 * assetScale), (int)(50 * assetScale))); + var condensedResinRes = crop2.Find(condensedResinTopIconRa); if (condensedResinRes.IsExist()) { // 找出 icon 的位置 + 25 ~ icon 的位置+45 就是浓缩树脂的数字,数字宽20 - var condensedResinCountRect = new Rect(condensedResinRes.Right + (int)(25 * assetScale), condensedResinRes.Y, (int)(20 * assetScale), condensedResinRes.Height); - string cnt40 = OcrFactory.Paddle.OcrWithoutDetector(region.DeriveCrop(condensedResinCountRect).SrcMat); + var condensedResinCountRect = new Rect(crop2.X + condensedResinRes.Right + (int)(20 * assetScale), (int)(37 * assetScale), (int)(70 * assetScale), (int)(24 * assetScale)); + using ImageRegion countRegion = region.DeriveCrop(condensedResinCountRect); + using Mat threshold = countRegion.CacheGreyMat.Threshold(180, 255, ThresholdTypes.Binary); + using Mat bitwiseNot = new Mat(); + Cv2.BitwiseNot(threshold, bitwiseNot); + //Cv2.ImShow("bitwise", bitwise); + //Cv2.WaitKey(); + string cnt40 = ocrService.OcrWithoutDetector(bitwiseNot); status.CondensedResinCount = StringUtils.TryExtractPositiveInt(cnt40, 0); } diff --git a/BetterGenshinImpact/GameTask/AutoFight/Assets/1920x1080/use_condensed_resin.png b/BetterGenshinImpact/GameTask/AutoFight/Assets/1920x1080/use_condensed_resin.png deleted file mode 100644 index 3df04cbd..00000000 Binary files a/BetterGenshinImpact/GameTask/AutoFight/Assets/1920x1080/use_condensed_resin.png and /dev/null differ diff --git a/BetterGenshinImpact/GameTask/AutoFight/Assets/AutoFightAssets.cs b/BetterGenshinImpact/GameTask/AutoFight/Assets/AutoFightAssets.cs index bbd30103..6fd55764 100644 --- a/BetterGenshinImpact/GameTask/AutoFight/Assets/AutoFightAssets.cs +++ b/BetterGenshinImpact/GameTask/AutoFight/Assets/AutoFightAssets.cs @@ -1,4 +1,4 @@ -using BetterGenshinImpact.Core.Recognition; +using BetterGenshinImpact.Core.Recognition; using BetterGenshinImpact.GameTask.Model; using OpenCvSharp; using System.Collections.Generic; @@ -23,15 +23,9 @@ public class AutoFightAssets : BaseAssets public RecognitionObject ArtifactAreaRa; public RecognitionObject ExitRa; public RecognitionObject ClickAnyCloseTipRa; - public RecognitionObject UseCondensedResinRa; - // 树脂状态 - public RecognitionObject CondensedResinCountRa; - public RecognitionObject FragileResinCountRa; // 自动秘境 // public RecognitionObject LockIconRa; // 锁定辅助图标 - public RecognitionObject CondensedResinTopIconRa; - public RecognitionObject OriginalResinTopIconRa; public Dictionary AvatarCostumeMap; @@ -44,7 +38,7 @@ public class AutoFightAssets : BaseAssets // 小道具位置 public Rect GadgetRect; - + public RecognitionObject AbnormalIconRa; private AutoFightAssets() @@ -59,7 +53,7 @@ public class AutoFightAssets : BaseAssets (int)(41 * AssetScale), (int)(18 * AssetScale)); QRect = new Rect(CaptureRect.Width - (int)(157 * AssetScale), CaptureRect.Height - (int)(165 * AssetScale), (int)(110 * AssetScale), (int)(110 * AssetScale)); - ZCooldownRect = new Rect(CaptureRect.Width - (int)(130 * AssetScale), (int)(814 * AssetScale), + ZCooldownRect = new Rect(CaptureRect.Width - (int)(130 * AssetScale), (int)(814 * AssetScale), (int)(60 * AssetScale), (int)(24 * AssetScale)); // 小道具位置 1920-133,800,60,50 GadgetRect = new Rect(CaptureRect.Width - (int)(133 * AssetScale), (int)(800 * AssetScale), @@ -205,7 +199,7 @@ public class AutoFightAssets : BaseAssets Name = "ArtifactArea", RecognitionType = RecognitionTypes.TemplateMatch, TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "artifact_flower_logo.png"), - RegionOfInterest = new Rect(CaptureRect.Width / 2,0,CaptureRect.Width / 2, CaptureRect.Height), + RegionOfInterest = new Rect(CaptureRect.Width / 2, 0, CaptureRect.Width / 2, CaptureRect.Height), DrawOnWindow = false }.InitTemplate(); @@ -219,15 +213,6 @@ public class AutoFightAssets : BaseAssets DrawOnWindow = false }.InitTemplate(); - UseCondensedResinRa = new RecognitionObject - { - Name = "UseCondensedResin", - RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "use_condensed_resin.png"), - RegionOfInterest = new Rect(0, CaptureRect.Height / 2, CaptureRect.Width / 2, CaptureRect.Height / 2), - DrawOnWindow = false - }.InitTemplate(); - ExitRa = new RecognitionObject { Name = "Exit", @@ -237,23 +222,6 @@ public class AutoFightAssets : BaseAssets DrawOnWindow = false }.InitTemplate(); - CondensedResinCountRa = new RecognitionObject - { - Name = "CondensedResinCount", - RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "condensed_resin_count.png"), - RegionOfInterest = new Rect(CaptureRect.Width / 2, CaptureRect.Height / 3 * 2, CaptureRect.Width / 2, CaptureRect.Height / 3), - DrawOnWindow = false - }.InitTemplate(); - FragileResinCountRa = new RecognitionObject - { - Name = "FragileResinCount", - RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "fragile_resin_count.png"), - RegionOfInterest = new Rect(CaptureRect.Width / 2, CaptureRect.Height / 3 * 2, CaptureRect.Width / 2, CaptureRect.Height / 3), - DrawOnWindow = false - }.InitTemplate(); - // 自动秘境 // LockIconRa = new RecognitionObject // { @@ -263,30 +231,14 @@ public class AutoFightAssets : BaseAssets // RegionOfInterest = new Rect(CaptureRect.Width - (int)(215 * AssetScale), 0, (int)(215 * AssetScale), (int)(80 * AssetScale)), // DrawOnWindow = false // }.InitTemplate(); - CondensedResinTopIconRa = new RecognitionObject - { - Name = "CondensedResinTopIcon", - RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "condensed_resin_top_icon.png"), - RegionOfInterest = new Rect((int)(1270 * AssetScale), (int)(25 * AssetScale), (int)(520 * AssetScale), (int)(45 * AssetScale)), - DrawOnWindow = false - }.InitTemplate(); - OriginalResinTopIconRa = new RecognitionObject - { - Name = "OriginalResinTopIcon", - RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "original_resin_top_icon.png"), - RegionOfInterest = new Rect(CaptureRect.Width - (int)(450 * AssetScale), (int)(25 * AssetScale), (int)(265 * AssetScale), (int)(45 * AssetScale)), - DrawOnWindow = false - }.InitTemplate(); + AbnormalIconRa = new RecognitionObject { Name = "AbnormalIcon", RecognitionType = RecognitionTypes.TemplateMatch, TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "abnormal_icon.png"), - RegionOfInterest = new Rect(0,(int)(CaptureRect.Height*0.08), (int)(CaptureRect.Width*0.04), (int)(CaptureRect.Height*0.07)), + RegionOfInterest = new Rect(0, (int)(CaptureRect.Height * 0.08), (int)(CaptureRect.Width * 0.04), (int)(CaptureRect.Height * 0.07)), DrawOnWindow = false }.InitTemplate(); - } } diff --git a/BetterGenshinImpact/GameTask/AutoFight/Assets/combat_avatar.json b/BetterGenshinImpact/GameTask/AutoFight/Assets/combat_avatar.json index faea52b2..28278b5f 100644 --- a/BetterGenshinImpact/GameTask/AutoFight/Assets/combat_avatar.json +++ b/BetterGenshinImpact/GameTask/AutoFight/Assets/combat_avatar.json @@ -1902,7 +1902,9 @@ { "alias": [ "伊涅芙", - "Ineffa" + "Ineffa", + "机器人", + "机娘" ], "burstCD": 15, "id": "10000116", @@ -1910,5 +1912,42 @@ "nameEn": "Ineffa", "skillCD": 16, "weapon": "13" + }, + { + "alias": [ + "菈乌玛", + "Lauma" + ], + "burstCD": 15, + "id": "10000119", + "name": "菈乌玛", + "nameEn": "Lauma", + "skillCD": 12, + "weapon": "10" + }, + { + "alias": [ + "菲林斯", + "Flins" + ], + "burstCD": 20, + "id": "10000120", + "name": "菲林斯", + "nameEn": "Flins", + "skillCD": 6, + "skillHoldCD": 16, + "weapon": "13" + }, + { + "alias": [ + "爱诺", + "Aino" + ], + "burstCD": 13.5, + "id": "10000121", + "name": "爱诺", + "nameEn": "Aino", + "skillCD": 10, + "weapon": "11" } ] \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/AutoFight/Model/Avatar.cs b/BetterGenshinImpact/GameTask/AutoFight/Model/Avatar.cs index eb611a47..844b8af4 100644 --- a/BetterGenshinImpact/GameTask/AutoFight/Model/Avatar.cs +++ b/BetterGenshinImpact/GameTask/AutoFight/Model/Avatar.cs @@ -175,6 +175,8 @@ public class Avatar break; } + Offset60Fix(i); + // Debug.WriteLine($"切换到{Index}号位"); // Cv2.ImWrite($"log/切换.png", region.SrcMat); Sleep(250, Ct); @@ -232,6 +234,8 @@ public class Avatar default: break; } + + Offset60Fix(i); Sleep(250, Ct); } @@ -277,11 +281,31 @@ public class Avatar default: break; } + + Offset60Fix(i); Sleep(250); } } + private void Offset60Fix(int i) + { + // 6.0 特殊逻辑 + if (i > 3 && CombatScenes.IndexRectOffset60Fix) + { + // 3次失败考虑是否偏移出现问题,修改偏移位置 + // 只有 草露 角色离队,然后跨地图传送后,会出现这个场景。也就是只有 偏移 -> 原始 的场景 + foreach (var avatar in CombatScenes.GetAvatars()) + { + var rect1 = avatar.IndexRect; + rect1.Y += 14; + avatar.IndexRect = rect1; + } + + CombatScenes.IndexRectOffset60Fix = false; + } + } + /// /// 是否出战状态 /// @@ -294,20 +318,24 @@ public class Avatar } else { - // 剪裁出IndexRect区域 - var indexRa = region.DeriveCrop(IndexRect); - // Cv2.ImWrite($"log/indexRa_{Name}.png", indexRa.SrcMat); - var count = OpenCvCommonHelper.CountGrayMatColor(indexRa.CacheGreyMat, 251, 255); - if (count * 1.0 / (IndexRect.Width * IndexRect.Height) > 0.5) - { - return false; - } - else - { - return true; - } + var white = IsIndexRectWhite(region, IndexRect); + return !white; } } + + private bool IsIndexRectWhite(ImageRegion region, Rect rect) + { + // 剪裁出IndexRect区域 + var indexRa = region.DeriveCrop(rect); + var mat = indexRa.CacheGreyMat; + var count = OpenCvCommonHelper.CountGrayMatColor(mat, 251, 255); + if (count * 1.0 / (mat.Width * mat.Height) > 0.5) + { + return true; + } + + return false; + } /// /// 是否出战状态 diff --git a/BetterGenshinImpact/GameTask/AutoFight/Model/CombatScenes.cs b/BetterGenshinImpact/GameTask/AutoFight/Model/CombatScenes.cs index bab92d4f..a0bf3488 100644 --- a/BetterGenshinImpact/GameTask/AutoFight/Model/CombatScenes.cs +++ b/BetterGenshinImpact/GameTask/AutoFight/Model/CombatScenes.cs @@ -40,6 +40,12 @@ public class CombatScenes : IDisposable App.ServiceProvider.GetRequiredService().CreateYoloPredictor(BgiOnnxModel.BgiAvatarSide); public int ExpectedTeamAvatarNum { get; private set; } = 4; + + + /// + /// 6.0 UI偏移标识 + /// + public bool IndexRectOffset60Fix { get; set; } /// /// 获取一个只读的Avatars @@ -100,6 +106,9 @@ public class CombatScenes : IDisposable avatarSideIconRectList = AutoFightAssets.Instance.AvatarSideIconRectList; avatarIndexRectList = AutoFightAssets.Instance.AvatarIndexRectList; } + + // 6.0 版本 队伍下的 草露 进度条 导致位置偏移 + IndexRectOffset60Fix = AvatarSideFixOffset(imageRegion, avatarSideIconRectList, avatarIndexRectList); // 识别队伍 var names = new string[avatarSideIconRectList.Count]; @@ -137,6 +146,53 @@ public class CombatScenes : IDisposable return this; } + + /// + /// 6.0 版本 队伍下的 草露 进度条 导致位置偏移 + /// + /// + /// + /// + /// + public bool AvatarSideFixOffset(ImageRegion imageRegion, List avatarSideIconRectList, List avatarIndexRectList) + { + // 角色序号 左上角 坐标偏移(+2, -5)后存在3个白色点,则认为存在 草露 进度条 + // 存在 草露 进度条时候整体上移 14 个像素 + int whitePointCount = 0; + foreach (var rectIndex in avatarIndexRectList) + { + int x = rectIndex.X + 2; + int y = rectIndex.Y - 5; + var color = imageRegion.SrcMat.At(y, x); + if (color is { Item0: 255, Item1: 255, Item2: 255 }) + { + whitePointCount++; + } + } + + if (whitePointCount >= 3) + { + Logger.LogInformation("检测到右侧队伍偏移,进行位置修正"); + for (int i = 0; i < avatarSideIconRectList.Count; i++) + { + var rect = avatarSideIconRectList[i]; + rect.Y -= 14; + avatarSideIconRectList[i] = rect; + } + + for (int i = 0; i < avatarIndexRectList.Count; i++) + { + var rect = avatarIndexRectList[i]; + rect.Y -= 14; + avatarIndexRectList[i] = rect; + } + + return true; + } + + return false; + } + public (string, string) ClassifyAvatarCnName(Image img, int index) { diff --git a/BetterGenshinImpact/GameTask/AutoFishing/Behaviours.cs b/BetterGenshinImpact/GameTask/AutoFishing/Behaviours.cs index 2aef44cc..3b4696a7 100644 --- a/BetterGenshinImpact/GameTask/AutoFishing/Behaviours.cs +++ b/BetterGenshinImpact/GameTask/AutoFishing/Behaviours.cs @@ -150,34 +150,13 @@ namespace BetterGenshinImpact.GameTask.AutoFishing logger.LogInformation("选择鱼饵 {Text}", blackboard.selectedBait.GetDescription()); // 寻找鱼饵 - using ImageRegion singleRowGrid = imageRegion.DeriveCrop(0.28 * imageRegion.Width, 0.37 * imageRegion.Height, 0.45 * imageRegion.Width, 0.22 * imageRegion.Height); - using Mat grey = singleRowGrid.SrcMat.CvtColor(ColorConversionCodes.BGR2GRAY); - using Mat canny = grey.Canny(20, 40); - - Cv2.FindContours(canny, out Point[][] contours, out _, RetrievalModes.External, ContourApproximationModes.ApproxSimple, null); - contours = contours - .Where(c => - { - Rect r = Cv2.BoundingRect(c); - if (r.Width < 0.065 * imageRegion.Width * 0.80) // 剔除太小的 - { - return false; - } - if (r.Height == 0) - { - return false; - } - return Math.Abs((float)r.Width / r.Height - 0.81) < 0.05; // 按形状筛选 - }).ToArray(); - IEnumerable boxes = contours.Select(Cv2.BoundingRect); - - foreach (Rect box in boxes) + var boxAndBaits = FindBait(imageRegion); + ; + foreach ((Rect box, string? predName) in boxAndBaits) { - using ImageRegion resRa = singleRowGrid.DeriveCrop(box); - using Mat img125 = resRa.SrcMat.GetGridIcon(); - (string predName, _) = GridIconsAccuracyTestTask.Infer(img125, this.session, this.prototypes); if (predName == blackboard.selectedBait.GetDescription()) { + using ImageRegion resRa = imageRegion.DeriveCrop(box); resRa.Click(); blackboard.Sleep(700); // 可能重复点击,所以固定界面点击下 @@ -225,6 +204,37 @@ namespace BetterGenshinImpact.GameTask.AutoFishing return BehaviourStatus.Running; } } + + public IEnumerable<(Rect, string?)> FindBait(ImageRegion imageRegion1080p) + { + using ImageRegion singleRowGrid = imageRegion1080p.DeriveCrop(0.28 * imageRegion1080p.Width, 0.37 * imageRegion1080p.Height, 0.45 * imageRegion1080p.Width, 0.22 * imageRegion1080p.Height); + using Mat grey = singleRowGrid.SrcMat.CvtColor(ColorConversionCodes.BGR2GRAY); + using Mat canny = grey.Canny(20, 40); + + Cv2.FindContours(canny, out Point[][] contours, out _, RetrievalModes.External, ContourApproximationModes.ApproxSimple, null); + contours = contours + .Where(c => + { + Rect r = Cv2.BoundingRect(c); + if (r.Width < 0.065 * imageRegion1080p.Width * 0.80) // 剔除太小的 + { + return false; + } + if (r.Height == 0) + { + return false; + } + return Math.Abs((float)r.Width / r.Height - 0.81) < 0.05; // 按形状筛选 + }).ToArray(); + IEnumerable boxes = contours.Select(Cv2.BoundingRect); + foreach (Rect box in boxes) + { + using ImageRegion resRa = singleRowGrid.DeriveCrop(box); + using Mat img125 = resRa.SrcMat.GetGridIcon(); + (string? predName, _) = GridIconsAccuracyTestTask.Infer(img125, this.session, this.prototypes); + yield return (new Rect(singleRowGrid.X + box.X, singleRowGrid.Y + box.Y, box.Width, box.Height), predName); + } + } } [Obsolete] diff --git a/BetterGenshinImpact/GameTask/AutoFishing/Model/BaitType.cs b/BetterGenshinImpact/GameTask/AutoFishing/Model/BaitType.cs index 3e269cc8..8f3049d7 100644 --- a/BetterGenshinImpact/GameTask/AutoFishing/Model/BaitType.cs +++ b/BetterGenshinImpact/GameTask/AutoFishing/Model/BaitType.cs @@ -21,5 +21,9 @@ public enum BaitType [Description("澄晶果粒饵")] SpinelgrainBait, [Description("温火饵")] - EmberglowBait + EmberglowBait, + [Description("槲梭饵")] + BerryBait, + [Description("清白饵")] + RefreshingLakkaBait } \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/AutoFishing/Model/BigFishType.cs b/BetterGenshinImpact/GameTask/AutoFishing/Model/BigFishType.cs index 6cf0f561..90162259 100644 --- a/BetterGenshinImpact/GameTask/AutoFishing/Model/BigFishType.cs +++ b/BetterGenshinImpact/GameTask/AutoFishing/Model/BigFishType.cs @@ -30,7 +30,11 @@ public class BigFishType public static readonly BigFishType Rapidfish = new("rapidfish", BaitType.SpinelgrainBait, "斗士急流鱼", 9); public static readonly BigFishType PhonyUnihornfish = new("phony unihornfish", BaitType.EmberglowBait, "燃素独角鱼", 10); public static readonly BigFishType MagmaRapidfish = new("magma rapidfish", BaitType.EmberglowBait, "炽岩斗士急流鱼", 9); + public static readonly BigFishType SecretSourceScoutSweeper = new ("secret source scout sweeper", BaitType.EmberglowBait, "秘源机关・巡戒使", 9); + public static readonly BigFishType MaulerShark = new ("mauler shark", BaitType.RefreshingLakkaBait, "凶凶鲨", 9); + public static readonly BigFishType CrystalEye = new("crystal eye", BaitType.RefreshingLakkaBait, "明眼鱼", 9); + public static readonly BigFishType AxeheadFish = new ("axehead fish", BaitType.BerryBait, "巨斧鱼", 9); public static IEnumerable Values { @@ -55,6 +59,9 @@ public class BigFishType yield return Rapidfish; yield return PhonyUnihornfish; yield return MagmaRapidfish; + yield return MaulerShark; + yield return CrystalEye; + yield return AxeheadFish; } } diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/tcg_character_card.json b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/tcg_character_card.json index 57fe0b0b..7aa58332 100644 --- a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/tcg_character_card.json +++ b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/tcg_character_card.json @@ -88,7 +88,7 @@ "nameEn": "diona", "type": "character", "name": "迪奥娜", - "hp": 10, + "hp": 12, "energy": 3, "element": "冰元素", "weapon": "弓", @@ -2145,7 +2145,7 @@ "nameEn": "amber", "type": "character", "name": "安柏", - "hp": 10, + "hp": 12, "energy": 2, "element": "火元素", "weapon": "弓", @@ -3922,6 +3922,83 @@ } ] }, + { + "id": 1414, + "nameEn": "iansan", + "type": "character", + "name": "伊安珊", + "hp": 12, + "energy": 2, + "element": "雷元素", + "weapon": "长柄武器", + "skills": [ + { + "nameEn": "weighted_spike", + "name": "负重锥击", + "skillTag": [ + "普通攻击" + ], + "cost": [ + { + "id": 1104, + "nameEn": "electro", + "type": "雷元素", + "count": 1 + }, + { + "id": 1109, + "nameEn": "unaligned_element", + "type": "无色元素", + "count": 2 + } + ] + }, + { + "nameEn": "thunderbolt_rush", + "name": "电掣雷驰", + "skillTag": [ + "元素战技" + ], + "cost": [ + { + "id": 1104, + "nameEn": "electro", + "type": "雷元素", + "count": 3 + } + ] + }, + { + "nameEn": "the_three_principles_of_power", + "name": "力的三原理", + "skillTag": [ + "元素爆发" + ], + "cost": [ + { + "id": 1104, + "nameEn": "electro", + "type": "雷元素", + "count": 3 + }, + { + "id": 1110, + "nameEn": "energy", + "type": "充能", + "count": 2 + } + ] + }, + { + "nameEn": "caloric_balancing_plan", + "name": "热量均衡计划", + "skillTag": [ + "被动技能" + ], + "cost": [] + } + ] + }, { "id": 1501, "nameEn": "sucrose", @@ -4835,6 +4912,75 @@ } ] }, + { + "id": 1514, + "nameEn": "yumemizuki_mizuki", + "type": "character", + "name": "梦见月瑞希", + "hp": 10, + "energy": 2, + "element": "风元素", + "weapon": "法器", + "skills": [ + { + "nameEn": "pure_heart_pure_dreams", + "name": "梦我梦心", + "skillTag": [ + "普通攻击" + ], + "cost": [ + { + "id": 1105, + "nameEn": "anemo", + "type": "风元素", + "count": 1 + }, + { + "id": 1109, + "nameEn": "unaligned_element", + "type": "无色元素", + "count": 2 + } + ] + }, + { + "nameEn": "aisa_utamakura_pilgrimage", + "name": "秋沙歌枕巡礼", + "skillTag": [ + "元素战技" + ], + "cost": [ + { + "id": 1105, + "nameEn": "anemo", + "type": "风元素", + "count": 3 + } + ] + }, + { + "nameEn": "anraku_secret_spring_therapy", + "name": "安乐秘汤疗法", + "skillTag": [ + "元素爆发" + ], + "cost": [ + { + "id": 1105, + "nameEn": "anemo", + "type": "风元素", + "count": 3 + }, + { + "id": 1110, + "nameEn": "energy", + "type": "充能", + "count": 2 + } + ] + } + ] + }, { "id": 1601, "nameEn": "ningguang", @@ -5553,7 +5699,7 @@ "nameEn": "xilonen", "type": "character", "name": "希诺宁", - "hp": 10, + "hp": 12, "energy": 2, "element": "岩元素", "weapon": "单手剑", @@ -6128,7 +6274,7 @@ "nameEn": "kaveh", "type": "character", "name": "卡维", - "hp": 10, + "hp": 12, "energy": 2, "element": "草元素", "weapon": "双手剑", @@ -6643,7 +6789,7 @@ "nameEn": "rhodeia_of_loch", "type": "character", "name": "纯水精灵·洛蒂娅", - "hp": 10, + "hp": 11, "energy": 3, "element": "水元素", "weapon": "其他武器", @@ -7322,6 +7468,83 @@ } ] }, + { + "id": 2305, + "nameEn": "lord_of_eroded_primal_fire", + "type": "character", + "name": "蚀灭的源焰之主", + "hp": 12, + "energy": 2, + "element": "火元素", + "weapon": "其他武器", + "skills": [ + { + "nameEn": "void_claw_strike", + "name": "虚界玄爪", + "skillTag": [ + "普通攻击" + ], + "cost": [ + { + "id": 1103, + "nameEn": "pyro", + "type": "火元素", + "count": 1 + }, + { + "id": 1109, + "nameEn": "unaligned_element", + "type": "无色元素", + "count": 2 + } + ] + }, + { + "nameEn": "eroded_flaming_feathers", + "name": "蚀灭火羽", + "skillTag": [ + "元素战技" + ], + "cost": [ + { + "id": 1103, + "nameEn": "pyro", + "type": "火元素", + "count": 3 + } + ] + }, + { + "nameEn": "severing_primal_fire", + "name": "斫劫源焰", + "skillTag": [ + "元素爆发" + ], + "cost": [ + { + "id": 1103, + "nameEn": "pyro", + "type": "火元素", + "count": 3 + }, + { + "id": 1110, + "nameEn": "energy", + "type": "充能", + "count": 2 + } + ] + }, + { + "nameEn": "resentment", + "name": "忿恨", + "skillTag": [ + "被动技能" + ], + "cost": [] + } + ] + }, { "id": 2401, "nameEn": "electro_hypostasis", @@ -8480,7 +8703,7 @@ "nameEn": "gluttonous_yumkasaur_mountain_king", "type": "character", "name": "贪食匿叶龙山王", - "hp": 7, + "hp": 8, "energy": 2, "element": "草元素", "weapon": "其他武器", diff --git a/BetterGenshinImpact/GameTask/AutoMusicGame/AutoAlbumTask.cs b/BetterGenshinImpact/GameTask/AutoMusicGame/AutoAlbumTask.cs index 2227a34b..8d7963ef 100644 --- a/BetterGenshinImpact/GameTask/AutoMusicGame/AutoAlbumTask.cs +++ b/BetterGenshinImpact/GameTask/AutoMusicGame/AutoAlbumTask.cs @@ -41,6 +41,7 @@ public class AutoAlbumTask(AutoMusicGameParam taskParam) : ISoloTask catch (Exception e) { Logger.LogError("自动音乐专辑任务异常:{Msg}", e.Message); + Logger.LogDebug(e, "自动音乐专辑任务异常详情"); Notify.Event(NotificationEvent.AlbumError).Error("自动音游专辑异常", e); } } diff --git a/BetterGenshinImpact/GameTask/AutoOpenChest/Assets/AutoOpenChestAssets.cs b/BetterGenshinImpact/GameTask/AutoOpenChest/Assets/AutoOpenChestAssets.cs index 5fd596e6..fce1f847 100644 --- a/BetterGenshinImpact/GameTask/AutoOpenChest/Assets/AutoOpenChestAssets.cs +++ b/BetterGenshinImpact/GameTask/AutoOpenChest/Assets/AutoOpenChestAssets.cs @@ -1,8 +1,8 @@ -using BetterGenshinImpact.Core.Recognition; +using BetterGenshinImpact.Core.Recognition; using BetterGenshinImpact.GameTask.Model; using OpenCvSharp; -namespace BetterGenshinImpact.GameTask.AutoFishing.Assets; +namespace BetterGenshinImpact.GameTask.AutoOpenChest.Assets; public class AutoOpenChestAssets : BaseAssets { @@ -14,7 +14,7 @@ public class AutoOpenChestAssets : BaseAssets #pragma warning disable CS8618 // 在退出构造函数时,不可为 null 的字段必须包含非 null 值。请考虑添加 "required" 修饰符或声明为可为 null。 private AutoOpenChestAssets() : base() { - Initialization(this.systemInfo); + Initialization(systemInfo); } protected AutoOpenChestAssets(ISystemInfo systemInfo) : base(systemInfo) diff --git a/BetterGenshinImpact/GameTask/AutoOpenChest/AutoOpenChestTask.cs b/BetterGenshinImpact/GameTask/AutoOpenChest/AutoOpenChestTask.cs index 73074c0b..089b5cd5 100644 --- a/BetterGenshinImpact/GameTask/AutoOpenChest/AutoOpenChestTask.cs +++ b/BetterGenshinImpact/GameTask/AutoOpenChest/AutoOpenChestTask.cs @@ -1,8 +1,6 @@ -using BetterGenshinImpact.Core.Simulator; +using BetterGenshinImpact.Core.Simulator; using BetterGenshinImpact.Core.Simulator.Extensions; -using BetterGenshinImpact.GameTask; -using BetterGenshinImpact.GameTask.AutoFishing.Assets; -using BetterGenshinImpact.GameTask.Common.BgiVision; +using BetterGenshinImpact.GameTask.AutoOpenChest.Assets; using BetterGenshinImpact.GameTask.Model.Area; using Microsoft.Extensions.Logging; using System; @@ -11,8 +9,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using static BetterGenshinImpact.GameTask.Common.TaskControl; -using static TorchSharp.torch.distributions.constraints; -namespace GameTask.AutoOpenChest; +namespace BetterGenshinImpact.GameTask.AutoOpenChest; /// /// 识别宝箱图标,走向宝箱并开启。 @@ -72,7 +69,7 @@ public class AutoOpenChestTask : ISoloTask } else { - var gap = (ra.Width / 2) - chestIcon.X; + var gap = ra.Width / 2 - chestIcon.X; int rate = 2; Simulation.SendInput.Mouse.MoveMouseBy(gap / rate, 0); } diff --git a/BetterGenshinImpact/GameTask/AutoPick/AutoPickTrigger.cs b/BetterGenshinImpact/GameTask/AutoPick/AutoPickTrigger.cs index 7ba28385..f77cfd72 100644 --- a/BetterGenshinImpact/GameTask/AutoPick/AutoPickTrigger.cs +++ b/BetterGenshinImpact/GameTask/AutoPick/AutoPickTrigger.cs @@ -12,6 +12,7 @@ using OpenCvSharp; using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Text.Json; using System.Text.RegularExpressions; using System.Threading; @@ -245,17 +246,34 @@ public partial class AutoPickTrigger : ITaskTrigger else { var textMat = new Mat(content.CaptureRectArea.SrcMat, textRect); - var boundingRect = GetWhiteTextBoundingRect(textMat); + var boundingRect = TextRectExtractor.GetTextBoundingRect(textMat, out var bin); // 如果找到有效区域 - if (boundingRect.Width > 5 && boundingRect.Height > 5) + if (boundingRect.X <20 && boundingRect.Width > 5 && boundingRect.Height > 5) { // 截取只包含文字的区域 var textOnlyMat = new Mat(textMat, new Rect(0, 0, - boundingRect.Right + 3 < textMat.Width ? boundingRect.Right + 3 : textMat.Width, textMat.Height)); + boundingRect.Right + 5 < textMat.Width ? boundingRect.Right + 5 : textMat.Width, textMat.Height)); text = OcrFactory.Paddle.OcrWithoutDetector(textOnlyMat); + + // if (RuntimeHelper.IsDebug) + // { + // // 如果不等于正确文字,则保存图片 + // if (text != "烹饪") + // { + // var path = Global.Absolute("log/pick"); + // Directory.CreateDirectory(path); + // var str = $"{DateTime.Now:yyyyMMddHHmmssfff}"; + // // textMat.SaveImage(Path.Combine(path, $"pick_ocr_ori_{str}.png")); + // // 画上 boundingRect + // Cv2.Rectangle(textMat, boundingRect, new Scalar(0, 0, 255), 1); + // textMat.SaveImage(Path.Combine(path, $"pick_ocr_rect_{str}.png")); + // bin.SaveImage(Path.Combine(path, $"bin_{str}.png")); + // } + // } } else { + Debug.WriteLine("-- 无法识别到有效文字区域,尝试直接OCR DET"); text = OcrFactory.Paddle.Ocr(textMat); } } @@ -263,6 +281,9 @@ public partial class AutoPickTrigger : ITaskTrigger speedTimer.Record("文字识别"); if (!string.IsNullOrEmpty(text)) { + // 处理OCR识别结果,清理无效字符并确保引号配对 + text = ProcessOcrText(text); + // 唯一一个动态拾取项,特殊处理,不拾取 if (text.Contains("长时间")) { @@ -275,21 +296,20 @@ public partial class AutoPickTrigger : ITaskTrigger { return; } + // 挪德卡莱聚所中文名特殊处理,不拾取 + if (text.Contains("聚所") && (text.Contains("霜月") || text.Contains("叮铃") || + text.Contains("眶螂") || text.Contains("蛋卷") || text.Contains("坊"))) + { + return; + } // 单个字符不拾取 - var simpleText = PunctuationAndSpacesRegex().Replace(text, ""); - if (simpleText.Length <= 1) - { - return; - } - - // 纯英文不拾取 - if (StringUtils.IsPureEnglish(text)) + if (text.Length <= 1) { return; } - if (config.WhiteListEnabled && (_whiteList.Contains(text) || _whiteList.Contains(simpleText))) + if (config.WhiteListEnabled && _whiteList.Contains(text)) { LogPick(content, text); Simulation.SendInput.Keyboard.KeyPress(AutoPickAssets.Instance.PickVk); @@ -304,7 +324,7 @@ public partial class AutoPickTrigger : ITaskTrigger return; } - if (config.BlackListEnabled && (_blackList.Contains(text) || _blackList.Contains(simpleText))) + if (config.BlackListEnabled && _blackList.Contains(text)) { return; } @@ -372,8 +392,114 @@ public partial class AutoPickTrigger : ITaskTrigger _prevClickFrameIndex = content.FrameIndex; } - [GeneratedRegex(@"^[\p{P} ]+|[\p{P} ]+$")] - private static partial Regex PunctuationAndSpacesRegex(); + /// + /// 高性能处理OCR识别的文字结果 + /// 1. 替换【、[ 为「,替换】、] 为」 + /// 2. 清理左边非「字符和中文的字符 + /// 3. 清理右边非」字符和中文的字符 + /// 4. 确保引号配对:有「必有」,有」必有「 + /// + /// OCR识别的原始文字 + /// 处理后的文字 + private static string ProcessOcrText(string text) + { + if (string.IsNullOrEmpty(text)) + return text; + + // 0. 首先替换相似的括号字符并删除换行符、空格,使用Span进行原地替换以获得最佳性能 + Span chars = stackalloc char[text.Length]; + text.AsSpan().CopyTo(chars); + + int writeIndex = 0; + bool hasChanges = false; + + for (int i = 0; i < chars.Length; i++) + { + char c = chars[i]; + + // 跳过换行符、回车符、空格、制表符等空白字符 + if (char.IsWhiteSpace(c)) + { + hasChanges = true; + continue; + } + + // 替换括号字符 + if (c == '【' || c == '[') + { + chars[writeIndex++] = '「'; + hasChanges = true; + } + else if (c == '】' || c == ']') + { + chars[writeIndex++] = '」'; + hasChanges = true; + } + else + { + chars[writeIndex++] = c; + } + } + + // 如果有变化,使用处理后的字符;否则使用原字符串的Span + ReadOnlySpan span = hasChanges ? chars.Slice(0, writeIndex) : text.AsSpan(); + int start = 0; + int end = span.Length - 1; + + // 1. 从左边开始,删除非「字符和中文的字符 + while (start <= end) + { + char c = span[start]; + if (c == '「' || (c >= 0x4E00 && c <= 0x9FFF)) // 「字符或中文字符 + break; + start++; + } + + // 2. 从右边开始,删除非」字符和中文的字符 + while (end >= start) + { + char c = span[end]; + if (c == '」' || c == '!' || (c >= 0x4E00 && c <= 0x9FFF)) // 」字符或中文字符 + break; + end--; + } + + // 如果所有字符都被删除了 + if (start > end) + return string.Empty; + + // 获取清理后的文字 + var cleanedSpan = span.Slice(start, end - start + 1); + + // 3. 检查并补充引号配对 + bool hasLeftQuote = false; + bool hasRightQuote = false; + + // 快速扫描是否存在引号 + for (int i = 0; i < cleanedSpan.Length; i++) + { + if (cleanedSpan[i] == '「') + hasLeftQuote = true; + else if (cleanedSpan[i] == '」') + hasRightQuote = true; + } + + // 根据引号配对规则补充 + if (hasLeftQuote && !hasRightQuote) + { + // 有「但没有」,在末尾补充」 + Debug.WriteLine("补充缺失的右引号"); + return string.Concat(cleanedSpan, "」"); + } + else if (hasRightQuote && !hasLeftQuote) + { + // 有」但没有「,在开头补充「 + Debug.WriteLine("补充缺失的左引号"); + return string.Concat("「", cleanedSpan); + } + + return cleanedSpan.ToString(); + } } \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/AutoPick/TextRectExtractor.cs b/BetterGenshinImpact/GameTask/AutoPick/TextRectExtractor.cs new file mode 100644 index 00000000..ac15db89 --- /dev/null +++ b/BetterGenshinImpact/GameTask/AutoPick/TextRectExtractor.cs @@ -0,0 +1,79 @@ +using System; +using System.Linq; + +namespace BetterGenshinImpact.GameTask.AutoPick; + +using OpenCvSharp; + +public static class TextRectExtractor +{ + /// + /// 从图片中提取文字范围(假定文字从最左边贴边开始,向右连续) + /// 结果矩形固定 x=0,y=0,h=原图高度,只计算连续文字宽度。 + /// + public static Rect GetTextBoundingRect(Mat textMat, out Mat bin) + { + // 转换为灰度图 + Mat gray = new Mat(); + if (textMat.Channels() == 3) + { + Cv2.CvtColor(textMat, gray, ColorConversionCodes.BGR2GRAY); + } + else + { + gray = textMat.Clone(); + } + + // 使用阈值160进行二值化处理 + bin = new Mat(); + Cv2.Threshold(gray, bin, 160, 255, ThresholdTypes.Binary); + + // 形态学操作:先腐蚀后膨胀,去除噪点并保持文字完整 + Mat kernel = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(3, 3)); + Cv2.Erode(bin, bin, kernel, iterations: 1); + Cv2.Dilate(bin, bin, kernel, iterations: 2); + kernel.Dispose(); + gray.Dispose(); + return ProjectionRect(textMat, bin); + } + + private static Rect ProjectionRect(Mat textMat, Mat bin) + { + // 投影:对行做 ReduceSum,得到 1 x width 的列和 + using var projection = new Mat(); + Cv2.Reduce(bin, projection, 0, ReduceTypes.Sum, MatType.CV_32S); + int width = projection.Cols; + projection.GetArray(out int[] colSums); + + int maxGap = 30; // 允许的最大连续空列数 + int gapCount = 0; + int lastNonEmpty = -1; + + for (int x = 0; x < width; x++) + { + bool hasInk = colSums[x] > 0; + if (hasInk) + { + lastNonEmpty = x; + gapCount = 0; + } + else + { + gapCount++; + if (gapCount > maxGap) + { + break; + } + } + } + + if (lastNonEmpty == -1) + { + // 没有检测到文字 + return new Rect(); + } + + Rect boundingRect = new Rect(0, 0, lastNonEmpty, textMat.Height); + return boundingRect; + } +} diff --git a/BetterGenshinImpact/GameTask/AutoStygianOnslaught/AutoStygianOnslaughtTask.cs b/BetterGenshinImpact/GameTask/AutoStygianOnslaught/AutoStygianOnslaughtTask.cs index 76472706..eeaac5f6 100644 --- a/BetterGenshinImpact/GameTask/AutoStygianOnslaught/AutoStygianOnslaughtTask.cs +++ b/BetterGenshinImpact/GameTask/AutoStygianOnslaught/AutoStygianOnslaughtTask.cs @@ -1,5 +1,6 @@ using BetterGenshinImpact.Core.BgiVision; using BetterGenshinImpact.Core.Recognition; +using BetterGenshinImpact.Core.Recognition.OCR; using BetterGenshinImpact.Core.Simulator; using BetterGenshinImpact.Core.Simulator.Extensions; using BetterGenshinImpact.GameTask.AutoArtifactSalvage; @@ -254,7 +255,7 @@ public class AutoStygianOnslaughtTask : ISoloTask var ra = CaptureToRectArea(); var ocrList = ra.FindMulti(RecognitionObject.OcrThis); - if (ocrList.Any(o => o.Text.Contains("好友挑战")) && ocrList.Any(o => o.Text.Contains("开始挑战"))) + if (ocrList.Any(o => o.Text.Contains("角色预览")) && ocrList.Any(o => o.Text.Contains("开始挑战"))) { // 选择boss _logger.LogInformation($"{Name}:选择BOSS编号{{Text}}", _taskParam.BossNum); @@ -398,7 +399,7 @@ public class AutoStygianOnslaughtTask : ISoloTask { // 自动刷干树脂 // 识别树脂状况 - var resinStatus = ResinStatus.RecogniseFromRegion(ra3); + var resinStatus = ResinStatus.RecogniseFromRegion(ra3, TaskContext.Instance().SystemInfo, OcrFactory.Paddle); resinStatus.Print(_logger); if (resinStatus is { CondensedResinCount: <= 0, OriginalResinCount: < 20 }) @@ -627,4 +628,4 @@ public class AutoStygianOnslaughtTask : ISoloTask await Delay(3000, _ct); } -} \ No newline at end of file +} diff --git a/BetterGenshinImpact/GameTask/AutoTrackPath/Assets/tp.json b/BetterGenshinImpact/GameTask/AutoTrackPath/Assets/tp.json index 93241ed8..621edb83 100644 --- a/BetterGenshinImpact/GameTask/AutoTrackPath/Assets/tp.json +++ b/BetterGenshinImpact/GameTask/AutoTrackPath/Assets/tp.json @@ -12209,6 +12209,807 @@ "country": "纳塔", "name": "传送锚点", "area": "悠悠集市" + }, + { + "id": "1700", + "gadgetId": "70600001", + "gadgetType": "TransPointFirst", + "type": "Goddess", + "position": [ + 1732.4111328125, + 0, + 9492.888671875 + ], + "tranPosition": [ + 1738.2041015625, + 0, + 9485.640625 + ], + "country": "挪德卡莱", + "name": "新月神像", + "area": "伦波岛" + }, + { + "id": "1701", + "gadgetId": "70600001", + "gadgetType": "TransPointSecond", + "type": "Dungeon", + "position": [ + 1829.96875, + 0, + 9968.9326171875 + ], + "tranPosition": [ + 1832.07861328125, + 0, + 9966.6796875 + ], + "country": "挪德卡莱", + "name": "无光的深都", + "description": "材料本", + "area": "伦波岛" + }, + { + "id": "1702", + "gadgetId": "70600001", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 1426.26953125, + 0, + 9364.361328125 + ], + "tranPosition": [ + 1435.646484375, + 0, + 9367.15625 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "那夏镇" + }, + { + "id": "1703", + "gadgetId": "70600001", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 1661.486328125, + 0, + 9452.7646484375 + ], + "tranPosition": [ + 1660.6416015625, + 0, + 9458.02734375 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "那夏镇" + }, + { + "id": "1704", + "gadgetId": "70600001", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 1628.921875, + 0, + 9529.51171875 + ], + "tranPosition": [ + 1637.3427734375, + 0, + 9537.419921875 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "那夏镇" + }, + { + "id": "1705", + "gadgetId": "70600001", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 1728.60400390625, + 0, + 9657.93359375 + ], + "tranPosition": [ + 1724.68017578125, + 0, + 9657.390625 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "伦波岛" + }, + { + "id": "1706", + "gadgetId": "70600001", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 1913.2666015625, + 0, + 9745.7763671875 + ], + "tranPosition": [ + 1916.314453125, + 0, + 9746.0458984375 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "伦波岛" + }, + { + "id": "1707", + "gadgetId": "70600001", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 2349.251953125, + 0, + 9250.142578125 + ], + "tranPosition": [ + 2349.97314453125, + 0, + 9247.923828125 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "伦波岛" + }, + { + "id": "1708", + "gadgetId": "70600001", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 2503.34375, + 0, + 8952.8544921875 + ], + "tranPosition": [ + 2498.614501953125, + 0, + 8945.59375 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "伦波岛" + }, + { + "id": "1709", + "gadgetId": "70600001", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 1808.001953125, + 0, + 9727.181640625 + ], + "tranPosition": [ + 1803.5458984375, + 0, + 9711.166015625 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "北方训练场" + }, + { + "id": "1710", + "gadgetId": "70600001", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 1717.00439453125, + 0, + 9874.6220703125 + ], + "tranPosition": [ + 1718.310546875, + 0, + 9885.201171875 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "北方训练场" + }, + { + "id": "1711", + "gadgetId": "70600001", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 1538.5693359375, + 0, + 10020.7421875 + ], + "tranPosition": [ + 1539.25537109375, + 0, + 10018.369140625 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "叮铃哐啷蛋卷工坊" + }, + { + "id": "1712", + "gadgetId": "70600001", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 2206.139892578125, + 0, + 8836.6396484375 + ], + "tranPosition": [ + 2203.386474609375, + 0, + 8833.7333984375 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "刻拉蒂之眼" + }, + { + "id": "1713", + "gadgetId": "70600001", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 2846.696044921875, + 0, + 9188.033203125 + ], + "tranPosition": [ + 2851.84765625, + 0, + 9191.2724609375 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "空寂走廊" + }, + { + "id": "1714", + "gadgetId": "70600001", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 2239.43603515625, + 0, + 9228.87109375 + ], + "tranPosition": [ + 2246.41064453125, + 0, + 9224.48046875 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "蓝珀湖" + }, + { + "id": "1715", + "gadgetId": "70600001", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 2297.338623046875, + 0, + 9096.4111328125 + ], + "tranPosition": [ + 2296.144287109375, + 0, + 9099.3046875 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "蓝珀湖" + }, + { + "id": "1716", + "gadgetId": "70600001", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 1856.17041015625, + 0, + 9358.6474609375 + ], + "tranPosition": [ + 1865.16357421875, + 0, + 9370.08984375 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "苔骨荒原" + }, + { + "id": "1717", + "gadgetId": "70600001", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 1901.662109375, + 0, + 9220.4033203125 + ], + "tranPosition": [ + 1898.93798828125, + 0, + 9222.14453125 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "苔骨荒原" + }, + { + "id": "1718", + "gadgetId": "70600001", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 2107.943603515625, + 0, + 9354.046875 + ], + "tranPosition": [ + 2103.78955078125, + 0, + 9351.6474609375 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "苔骨荒原" + }, + { + "id": "1719", + "gadgetId": "70600001", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 2522.204345703125, + 0, + 9211.5556640625 + ], + "tranPosition": [ + 2526.032470703125, + 0, + 9207.884765625 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "苔骨荒原" + }, + { + "id": "1720", + "gadgetId": "70600001", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 2133.54931640625, + 0, + 9559.87890625 + ], + "tranPosition": [ + 2134.95654296875, + 0, + 9562.84375 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "星砂滩" + }, + { + "id": "1721", + "gadgetId": "70600002", + "gadgetType": "TransPointFirst", + "type": "Goddess", + "position": [ + 1652.9140625, + 0, + 10408.8828125 + ], + "tranPosition": [ + 1657.12158203125, + 0, + 10416.3681640625 + ], + "country": "挪德卡莱", + "name": "新月神像", + "area": "希汐岛" + }, + { + "id": "1722", + "gadgetId": "70600002", + "gadgetType": "TransPointSecond", + "type": "Dungeon", + "position": [ + 1938.21826171875, + 0, + 10824.91015625 + ], + "tranPosition": [ + 1935.74609375, + 0, + 10828.0751953125 + ], + "country": "挪德卡莱", + "name": "失落的月庭", + "description": "材料本", + "area": "希汐岛" + }, + { + "id": "1723", + "gadgetId": "70600002", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 1691.3837890625, + 0, + 10960.89453125 + ], + "tranPosition": [ + 1682.15673828125, + 0, + 10973.19921875 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "雷图礁" + }, + { + "id": "1724", + "gadgetId": "70600002", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 1589.37060546875, + 0, + 11061.8388671875 + ], + "tranPosition": [ + 1592.021484375, + 0, + 11060.626953125 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "雷图礁" + }, + { + "id": "1725", + "gadgetId": "70600002", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 1919.638671875, + 0, + 10525.666015625 + ], + "tranPosition": [ + 1931.4033203125, + 0, + 10524.564453125 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "沐光之台" + }, + { + "id": "1726", + "gadgetId": "70600002", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 2055.183349609375, + 0, + 10755.9384765625 + ], + "tranPosition": [ + 2060.8837890625, + 0, + 10754.037109375 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "守誓者的圣所" + }, + { + "id": "1727", + "gadgetId": "70600002", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 1854.90283203125, + 0, + 10386.330078125 + ], + "tranPosition": [ + 1854.55810546875, + 0, + 10391.689453125 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "霜月之坊" + }, + { + "id": "1728", + "gadgetId": "70600002", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 1783.83056640625, + 0, + 10552.1005859375 + ], + "tranPosition": [ + 1793.5361328125, + 0, + 10552.92578125 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "希汐岛" + }, + { + "id": "1729", + "gadgetId": "70600002", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 1794.2666015625, + 0, + 10782.90234375 + ], + "tranPosition": [ + 1793.4140625, + 0, + 10781.017578125 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "希汐岛" + }, + { + "id": "1730", + "gadgetId": "70600002", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 2277.250244140625, + 0, + 10790.84375 + ], + "tranPosition": [ + 2277.42919921875, + 0, + 10788.6953125 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "希汐岛" + }, + { + "id": "1731", + "gadgetId": "70600003", + "gadgetType": "TransPointFirst", + "type": "Goddess", + "position": [ + 3045.697265625, + 0, + 9428.623046875 + ], + "tranPosition": [ + 3050.987548828125, + 0, + 9426.021484375 + ], + "country": "挪德卡莱", + "name": "新月神像", + "area": "帕哈岛" + }, + { + "id": "1732", + "gadgetId": "70600003", + "gadgetType": "TransPointSecond", + "type": "Dungeon", + "position": [ + 3151.607666015625, + 0, + 9378.9453125 + ], + "tranPosition": [ + 3150.29931640625, + 0, + 9375.095703125 + ], + "country": "挪德卡莱", + "name": "霜凝的机枢", + "description": "圣遗物本", + "area": "帕哈岛" + }, + { + "id": "1733", + "gadgetId": "70600003", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 3593.0986328125, + 0, + 9873.548828125 + ], + "tranPosition": [ + 3588.912109375, + 0, + 9874.380859375 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "北港" + }, + { + "id": "1734", + "gadgetId": "70600003", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 3303.773193359375, + 0, + 9203.404296875 + ], + "tranPosition": [ + 3301.760986328125, + 0, + 9204.46875 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "绯沙盐沼" + }, + { + "id": "1735", + "gadgetId": "70600003", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 3543.756103515625, + 0, + 9310.923828125 + ], + "tranPosition": [ + 3543.58056640625, + 0, + 9315.1259765625 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "绯沙盐沼" + }, + { + "id": "1736", + "gadgetId": "70600003", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 3025.621337890625, + 0, + 9822.30078125 + ], + "tranPosition": [ + 3031.96875, + 0, + 9825.13671875 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "南港" + }, + { + "id": "1737", + "gadgetId": "70600003", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 2829.17529296875, + 0, + 9899.525390625 + ], + "tranPosition": [ + 2832.10302734375, + 0, + 9899.994140625 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "南港" + }, + { + "id": "1738", + "gadgetId": "70600003", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 3090.505859375, + 0, + 9551.904296875 + ], + "tranPosition": [ + 3104.404541015625, + 0, + 9550.71484375 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "月矩力试验设计局" + }, + { + "id": "1739", + "gadgetId": "70600003", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 3130.807861328125, + 0, + 9703.982421875 + ], + "tranPosition": [ + 3144.232421875, + 0, + 9701.650390625 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "月矩力试验设计局" + }, + { + "id": "1740", + "gadgetId": "70600003", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 3397.948974609375, + 0, + 9594.3544921875 + ], + "tranPosition": [ + 3399.092041015625, + 0, + 9590.52734375 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "月矩力试验设计局" + }, + { + "id": "1741", + "gadgetId": "70600003", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 3139.771240234375, + 0, + 10106.5361328125 + ], + "tranPosition": [ + 3141.948486328125, + 0, + 10102.869140625 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "月矩力试验设计局" } ] }, diff --git a/BetterGenshinImpact/GameTask/AutoTrackPath/TpTask.cs b/BetterGenshinImpact/GameTask/AutoTrackPath/TpTask.cs index 80d0f215..ed0e3171 100644 --- a/BetterGenshinImpact/GameTask/AutoTrackPath/TpTask.cs +++ b/BetterGenshinImpact/GameTask/AutoTrackPath/TpTask.cs @@ -1,10 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using BetterGenshinImpact.Core.Recognition; using BetterGenshinImpact.Core.Recognition.OpenCv; using BetterGenshinImpact.Core.Script.Dependence; @@ -29,6 +22,13 @@ using BetterGenshinImpact.Helpers.Extensions; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; using OpenCvSharp; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Vanara.PInvoke; using static BetterGenshinImpact.GameTask.Common.TaskControl; @@ -37,12 +37,15 @@ namespace BetterGenshinImpact.GameTask.AutoTrackPath; /// /// 传送任务 /// -public class TpTask(CancellationToken ct) +public class TpTask { private readonly QuickTeleportAssets _assets = QuickTeleportAssets.Instance; private readonly Rect _captureRect = TaskContext.Instance().SystemInfo.ScaleMax1080PCaptureRect; private readonly double _zoomOutMax1080PRatio = TaskContext.Instance().SystemInfo.ZoomOutMax1080PRatio; private readonly TpConfig _tpConfig = TaskContext.Instance().Config.TpConfig; + private readonly CancellationToken ct; + private readonly CultureInfo cultureInfo; + private readonly IStringLocalizer stringLocalizer; /// /// 直接通过缩放比例按钮计算放大按钮的Y坐标 @@ -56,6 +59,14 @@ public class TpTask(CancellationToken ct) private const double DisplayTpPointZoomLevel = 4.4; // 传送点显示的时候的地图比例 + public TpTask(CancellationToken ct) + { + this.ct = ct; + TpTaskParam param = new TpTaskParam(); + this.cultureInfo = param.GameCultureInfo; + this.stringLocalizer = param.StringLocalizer; + } + /// /// 传送到七天神像 /// @@ -321,7 +332,7 @@ public class TpTask(CancellationToken ct) return; } //增加容错,小概率情况下碰到,前面点击传送失败 - capture.Find(_assets.TeleportButtonRo,rg=>rg.Click()); + capture.Find(_assets.TeleportButtonRo, rg => rg.Click()); await Delay(delayMs, ct); } @@ -859,7 +870,7 @@ public class TpTask(CancellationToken ct) return false; } - public async Task SwitchArea(string areaName) + internal async Task SwitchArea(string areaName) { GameCaptureRegion.GameRegionClick((rect, scale) => (rect.Width - 160 * scale, rect.Height - 60 * scale)); await Delay(300, ct); @@ -869,11 +880,8 @@ public class TpTask(CancellationToken ct) RecognitionType = RecognitionTypes.Ocr, RegionOfInterest = new Rect(ra.Width / 2, 0, ra.Width / 2, ra.Height) }); - IStringLocalizer stringLocalizer = App.GetService>() ?? throw new NullReferenceException(nameof(stringLocalizer)); - CultureInfo cultureInfo = new CultureInfo(TaskContext.Instance().Config.OtherConfig.GameCultureInfoName); - string minCountryLocalized = stringLocalizer.WithCultureGet(cultureInfo, areaName); - string commissionLocalized = stringLocalizer.WithCultureGet(cultureInfo, "委托"); - Region? matchRect = list.FirstOrDefault(r => !r.Text.Contains(commissionLocalized) && r.Text.Contains(minCountryLocalized)); + string minCountryLocalized = this.stringLocalizer.WithCultureGet(this.cultureInfo, areaName); + Region? matchRect = list.OrderByDescending(r => r.Y).FirstOrDefault(r => r.Text.Contains(minCountryLocalized)); if (matchRect == null) { Logger.LogWarning("切换区域失败:{Country}", areaName); @@ -891,7 +899,6 @@ public class TpTask(CancellationToken ct) await Delay(500, ct); } - public async Task Tp(string name) { // 通过大地图传送到指定传送点 @@ -907,7 +914,7 @@ public class TpTask(CancellationToken ct) public async Task ClickTpPoint(ImageRegion imageRegion) { // 1.判断是否在地图界面 - if(!Bv.IsInBigMapUi(imageRegion)) throw new RetryException("不在地图界面"); + if (!Bv.IsInBigMapUi(imageRegion)) throw new RetryException("不在地图界面"); // 2. 判断是否已经点出传送按钮 var hasTeleportButton = CheckTeleportButton(imageRegion); @@ -920,7 +927,7 @@ public class TpTask(CancellationToken ct) // 4. 循环判断选项列表是否有传送点(未激活点位也在里面) var hasMapChooseIcon = CheckMapChooseIcon(imageRegion); // 没有传送点说明不是传送点 - if(!hasMapChooseIcon) throw new TpPointNotActivate("选项列表不存在传送点"); + if (!hasMapChooseIcon) throw new TpPointNotActivate("选项列表不存在传送点"); var teleportButtonFound = await NewRetry.WaitForElementAppear( _assets.TeleportButtonRo, () => { }, @@ -931,10 +938,12 @@ public class TpTask(CancellationToken ct) if (!teleportButtonFound) throw new TpPointNotActivate("选项列表的传送点未激活"); await NewRetry.WaitForElementDisappear( _assets.TeleportButtonRo, - screen => { - screen.Find(_assets.TeleportButtonRo, ra => { - ra.Click(); - ra.Dispose(); + screen => + { + screen.Find(_assets.TeleportButtonRo, ra => + { + ra.Click(); + ra.Dispose(); }); }, ct, diff --git a/BetterGenshinImpact/GameTask/Common/Element/Assets/MapLazyAssets.en.resx b/BetterGenshinImpact/GameTask/AutoTrackPath/TpTask.en.resx similarity index 96% rename from BetterGenshinImpact/GameTask/Common/Element/Assets/MapLazyAssets.en.resx rename to BetterGenshinImpact/GameTask/AutoTrackPath/TpTask.en.resx index da0f2d6b..cb448646 100644 --- a/BetterGenshinImpact/GameTask/Common/Element/Assets/MapLazyAssets.en.resx +++ b/BetterGenshinImpact/GameTask/AutoTrackPath/TpTask.en.resx @@ -1,4 +1,4 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + revival + + \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/Common/BgiVision/BvResxHelper.fr.resx b/BetterGenshinImpact/GameTask/Common/BgiVision/BvResxHelper.fr.resx new file mode 100644 index 00000000..88ea3db7 --- /dev/null +++ b/BetterGenshinImpact/GameTask/Common/BgiVision/BvResxHelper.fr.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + réanimation + + \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/Common/BgiVision/BvResxHelper.zh-Hans.resx b/BetterGenshinImpact/GameTask/Common/BgiVision/BvResxHelper.zh-Hans.resx new file mode 100644 index 00000000..4530040e --- /dev/null +++ b/BetterGenshinImpact/GameTask/Common/BgiVision/BvResxHelper.zh-Hans.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 复苏 + + \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/Common/BgiVision/BvResxHelper.zh-Hant.resx b/BetterGenshinImpact/GameTask/Common/BgiVision/BvResxHelper.zh-Hant.resx new file mode 100644 index 00000000..05634cf6 --- /dev/null +++ b/BetterGenshinImpact/GameTask/Common/BgiVision/BvResxHelper.zh-Hant.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 復甦 + + \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/Common/BgiVision/BvStatus.cs b/BetterGenshinImpact/GameTask/Common/BgiVision/BvStatus.cs index f40b13aa..223a0e10 100644 --- a/BetterGenshinImpact/GameTask/Common/BgiVision/BvStatus.cs +++ b/BetterGenshinImpact/GameTask/Common/BgiVision/BvStatus.cs @@ -1,16 +1,18 @@ -using BetterGenshinImpact.GameTask.Common.Element.Assets; -using BetterGenshinImpact.GameTask.Model.Area; -using BetterGenshinImpact.GameTask.QuickTeleport.Assets; -using OpenCvSharp; -using System; -using System.Linq; -using System.Threading.Tasks; using BetterGenshinImpact.Core.Recognition; -using System.Threading; - using BetterGenshinImpact.GameTask.AutoFight.Assets; using BetterGenshinImpact.GameTask.AutoSkip.Assets; +using BetterGenshinImpact.GameTask.Common.Element.Assets; using BetterGenshinImpact.GameTask.GameLoading.Assets; +using BetterGenshinImpact.GameTask.Model.Area; +using BetterGenshinImpact.GameTask.QuickTeleport.Assets; +using BetterGenshinImpact.Helpers; +using Microsoft.Extensions.Localization; +using OpenCvSharp; +using System; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; namespace BetterGenshinImpact.GameTask.Common.BgiVision; @@ -23,7 +25,7 @@ namespace BetterGenshinImpact.GameTask.Common.BgiVision; /// public static partial class Bv { - + public static string WhichGameUi() { throw new NotImplementedException(); @@ -36,7 +38,7 @@ public static partial class Bv /// public static bool IsInMainUi(ImageRegion captureRa) { - return captureRa.Find(ElementAssets.Instance.PaimonMenuRo).IsExist() && !IsInRevivePrompt(captureRa); + return captureRa.Find(ElementAssets.Instance.PaimonMenuRo).IsExist() && !IsInRevivePrompt(captureRa); } /// @@ -59,7 +61,7 @@ public static partial class Bv return false; } - + /// /// 是否在秘境中 /// @@ -186,7 +188,7 @@ public static partial class Bv /// /// /// - public static bool IsInRevivePrompt(ImageRegion region) + internal static bool IsInRevivePrompt(ImageRegion region) { using var confirmRectArea = region.Find(AutoFightAssets.Instance.ConfirmRa); if (!confirmRectArea.IsEmpty()) @@ -196,7 +198,11 @@ public static partial class Bv RecognitionType = RecognitionTypes.Ocr, RegionOfInterest = new Rect(0, 0, region.Width, region.Height / 2) }); - if (list.Any(r => r.Text.Contains("复苏"))) + + CultureInfo cultureInfo = new CultureInfo(TaskContext.Instance().Config.OtherConfig.GameCultureInfoName); + IStringLocalizer stringLocalizer = App.GetService>() ?? throw new Exception(); + string revival = stringLocalizer.WithCultureGet(cultureInfo, "复苏"); + if (list.Any(r => r.Text.Contains(revival))) { return true; } @@ -273,6 +279,17 @@ public static partial class Bv { return await NewRetry.WaitForAction(() => IsInTalkUi(TaskControl.CaptureToRectArea()), ct, retryTimes, 500); } + + /// + /// 是否存在提示框/确认框 + /// 黑白款都能识别 + /// + /// + /// + public static bool IsInPromptDialog(ImageRegion captureRa) + { + return captureRa.Find(ElementAssets.Instance.PromptDialogLeftBottomStar).IsExist(); + } } public enum MotionStatus diff --git a/BetterGenshinImpact/GameTask/Common/Element/Assets/1920x1080/prompt_dialog_left_bottom_star.png b/BetterGenshinImpact/GameTask/Common/Element/Assets/1920x1080/prompt_dialog_left_bottom_star.png new file mode 100644 index 00000000..1f4123d4 Binary files /dev/null and b/BetterGenshinImpact/GameTask/Common/Element/Assets/1920x1080/prompt_dialog_left_bottom_star.png differ diff --git a/BetterGenshinImpact/GameTask/Common/Element/Assets/ElementAssets.cs b/BetterGenshinImpact/GameTask/Common/Element/Assets/ElementAssets.cs index 3c442ddb..e0e22a6d 100644 --- a/BetterGenshinImpact/GameTask/Common/Element/Assets/ElementAssets.cs +++ b/BetterGenshinImpact/GameTask/Common/Element/Assets/ElementAssets.cs @@ -8,6 +8,8 @@ namespace BetterGenshinImpact.GameTask.Common.Element.Assets; public class ElementAssets : BaseAssets { + public RecognitionObject PromptDialogLeftBottomStar; // 弹出框左下角的星星 + public RecognitionObject BtnWhiteConfirm; public RecognitionObject BtnWhiteCancel; public RecognitionObject BtnBlackConfirm; @@ -87,6 +89,14 @@ public class ElementAssets : BaseAssets private ElementAssets() { + PromptDialogLeftBottomStar = new RecognitionObject + { + Name = "PromptDialogLeftBottomStar", + RecognitionType = RecognitionTypes.TemplateMatch, + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "prompt_dialog_left_bottom_star.png"), + RegionOfInterest = new Rect(0, CaptureRect.Height / 2, CaptureRect.Width / 2, CaptureRect.Height - CaptureRect.Height / 2), + Threshold = 0.8, + }.InitTemplate(); // 按钮 BtnWhiteConfirm = new RecognitionObject { @@ -242,7 +252,7 @@ public class ElementAssets : BaseAssets Name = "fragileResinCount", RecognitionType = RecognitionTypes.TemplateMatch, TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "fragile_resin_count.png"), - RegionOfInterest = new Rect(CaptureRect.Width / 2, CaptureRect.Height / 2, CaptureRect.Width / 2, CaptureRect.Height / 2), + RegionOfInterest = new Rect(CaptureRect.Width / 2, CaptureRect.Height * 3/ 4, CaptureRect.Width / 3, CaptureRect.Height / 6), DrawOnWindow = true }.InitTemplate(); CondensedResinCount = new RecognitionObject diff --git a/BetterGenshinImpact/GameTask/Common/Element/Assets/MapLazyAssets.cs b/BetterGenshinImpact/GameTask/Common/Element/Assets/MapLazyAssets.cs index f2025520..6fdeb7cb 100644 --- a/BetterGenshinImpact/GameTask/Common/Element/Assets/MapLazyAssets.cs +++ b/BetterGenshinImpact/GameTask/Common/Element/Assets/MapLazyAssets.cs @@ -1,7 +1,5 @@ -using BetterGenshinImpact.Core.Config; +using BetterGenshinImpact.Core.Config; using BetterGenshinImpact.GameTask.AutoTrackPath.Model; -using BetterGenshinImpact.Service; -using OpenCvSharp; using System; using System.Collections.Generic; using System.IO; diff --git a/BetterGenshinImpact/GameTask/Common/Job/CountInventoryItem.cs b/BetterGenshinImpact/GameTask/Common/Job/CountInventoryItem.cs index 15cde499..bcb749ac 100644 --- a/BetterGenshinImpact/GameTask/Common/Job/CountInventoryItem.cs +++ b/BetterGenshinImpact/GameTask/Common/Job/CountInventoryItem.cs @@ -24,19 +24,19 @@ namespace BetterGenshinImpact.GameTask.Common.Job private readonly InputSimulator input = Simulation.SendInput; private CancellationToken ct; private readonly GridScreenName gridScreenName; - private readonly string foodName; + private readonly string itemName; - public CountInventoryItem(GridScreenName gridScreenName, string foodName) + public CountInventoryItem(GridScreenName gridScreenName, string itemName) { this.gridScreenName = gridScreenName; - this.foodName = foodName; + this.itemName = itemName; } public async Task Start(CancellationToken ct) { this.ct = ct; - logger.LogInformation("打开背包并在{grid}寻找{name}……", this.gridScreenName, this.foodName); + logger.LogInformation("打开背包并在{grid}寻找{name}……", this.gridScreenName, this.itemName); await new ReturnMainUiTask().Start(ct); await AutoArtifactSalvageTask.OpenInventory(this.gridScreenName, input, logger, this.ct); @@ -50,7 +50,7 @@ namespace BetterGenshinImpact.GameTask.Common.Job using Mat icon = itemRegion.SrcMat.GetGridIcon(); var result = GridIconsAccuracyTestTask.Infer(icon, session, prototypes); string predName = result.Item1; - if (predName == this.foodName) + if (predName == this.itemName) { string numStr = itemRegion.SrcMat.GetGridItemIconText(OcrFactory.Paddle); if (int.TryParse(numStr, out int num)) @@ -69,7 +69,7 @@ namespace BetterGenshinImpact.GameTask.Common.Job if (count == null) { count = -1; - logger.LogInformation("没有找到{name}", this.foodName); + logger.LogInformation("没有找到{name}", this.itemName); } await new ReturnMainUiTask().Start(ct); diff --git a/BetterGenshinImpact/GameTask/Common/Job/GoToCraftingBenchTask.cs b/BetterGenshinImpact/GameTask/Common/Job/GoToCraftingBenchTask.cs index c9e1e17c..60746d2c 100644 --- a/BetterGenshinImpact/GameTask/Common/Job/GoToCraftingBenchTask.cs +++ b/BetterGenshinImpact/GameTask/Common/Job/GoToCraftingBenchTask.cs @@ -94,7 +94,7 @@ public class GoToCraftingBenchTask { InitConfigList(); // 3. 点击合成树脂 - if (SelectedConfig.MinResinToKeep > 0){//开关判断,填写的数量大于0时启用 SelectedConfig.MinResinToKeep + if (SelectedConfig?.MinResinToKeep > 0){//开关判断,填写的数量大于0时启用 SelectedConfig.MinResinToKeep var fragileResinCount = 0; var condensedResinCount = 0; var fragileResinCountRa = ra.Find(ElementAssets.Instance.fragileResinCount); @@ -105,7 +105,7 @@ public class GoToCraftingBenchTask fragileResinCountRa.Width, fragileResinCountRa.Height); var count = OcrFactory.Paddle.OcrWithoutDetector(countArea.SrcMat); // Logger.LogInformation("识别原粹树脂数量:{Count}", count); - var match = System.Text.RegularExpressions.Regex.Match(count, @"(\d+)\s*[/17]\s*(4|40)"); + var match = System.Text.RegularExpressions.Regex.Match(count, @"(\d+)\s*[/17]\s*(6|60)"); if (match.Success) { var numericPart = match.Groups[1].Value; @@ -113,18 +113,30 @@ public class GoToCraftingBenchTask Logger.LogInformation("提取到的原粹树脂数量:{fragileResinCount}", fragileResinCount); } } - var condensedResinCountRa = ra.Find(ElementAssets.Instance.CondensedResinCount); - if (!condensedResinCountRa.IsEmpty()) + + //浓缩纠缠重试 + var condensed =await NewRetry.WaitForAction(() => { - // 图像右侧就是浓缩树脂数量 - var countArea = ra.DeriveCrop(condensedResinCountRa.X + condensedResinCountRa.Width, - condensedResinCountRa.Y, condensedResinCountRa.Width*5/3, condensedResinCountRa.Height); - var count = OcrFactory.Paddle.OcrWithoutDetector(countArea.CacheGreyMat); - condensedResinCount = StringUtils.TryParseInt(count); + var condensedResinCountRa = ra.Find(ElementAssets.Instance.CondensedResinCount); + if (!condensedResinCountRa.IsEmpty()) + { + // 图像右侧就是浓缩树脂数量 + var countArea = ra.DeriveCrop(condensedResinCountRa.X + condensedResinCountRa.Width, + condensedResinCountRa.Y, condensedResinCountRa.Width*5/3, condensedResinCountRa.Height); + var count = OcrFactory.Paddle.OcrWithoutDetector(countArea.CacheGreyMat); + condensedResinCount = StringUtils.TryParseInt(count); + } + return condensedResinCount >= 0 && condensedResinCount <=5; + },ct,3,200); + if (!condensed) + { + Simulation.SendInput.Keyboard.KeyPress(User32.VK.VK_ESCAPE); + await new ReturnMainUiTask().Start(ct); + throw new Exception($"识别浓缩树脂数量失败: {condensedResinCount}"); } - //todo 可加纠错机制判断树脂数量是否正确 + // 每次合成消耗的数量 - const int resinConsumedPerCraft = 40; + const int resinConsumedPerCraft = 60; // 需要保留的最小数量 int minResinToKeep = SelectedConfig.MinResinToKeep; // 可以用来合成的树脂数量 diff --git a/BetterGenshinImpact/GameTask/Common/Job/GoToSereniteaPotTask.cs b/BetterGenshinImpact/GameTask/Common/Job/GoToSereniteaPotTask.cs index 6501f8ff..f483b903 100644 --- a/BetterGenshinImpact/GameTask/Common/Job/GoToSereniteaPotTask.cs +++ b/BetterGenshinImpact/GameTask/Common/Job/GoToSereniteaPotTask.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; +using BetterGenshinImpact.Core.Config; using BetterGenshinImpact.Core.Recognition; using BetterGenshinImpact.Core.Simulator; using BetterGenshinImpact.Core.Simulator.Extensions; @@ -11,17 +6,25 @@ using BetterGenshinImpact.GameTask.AutoTrackPath; using BetterGenshinImpact.GameTask.Common.BgiVision; using BetterGenshinImpact.GameTask.Common.Element.Assets; using BetterGenshinImpact.GameTask.Model.Area; +using BetterGenshinImpact.GameTask.QuickSereniteaPot; using BetterGenshinImpact.GameTask.QuickTeleport.Assets; using BetterGenshinImpact.Helpers; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; +using Newtonsoft.Json; using OpenCvSharp; -using static BetterGenshinImpact.GameTask.Common.TaskControl; -using BetterGenshinImpact.Core.Config; +using System; +using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Globalization; using System.IO; using Newtonsoft.Json; using BetterGenshinImpact.GameTask.QuickSereniteaPot; +using BetterGenshinImpact.Core.Recognition.OCR; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using static BetterGenshinImpact.GameTask.Common.TaskControl; namespace BetterGenshinImpact.GameTask.Common.Job; @@ -83,9 +86,10 @@ internal class GoToSereniteaPotTask TaskContext.Instance().PostMessageSimulator.SimulateAction(GIActions.OpenMap); // 打开地图 await Delay(900, ct); - + // 进入 壶 - await ChangeCountryForce("尘歌壶", ct); + TpTask tpTask = new TpTask(ct); + await tpTask.SwitchArea("尘歌壶"); // 若未找到 ElementAssets.Instance.SereniteaPotRo 就是已经在尘歌壶了 var ra = CaptureToRectArea(); @@ -377,9 +381,7 @@ internal class GoToSereniteaPotTask RecognitionType = RecognitionTypes.Ocr, RegionOfInterest = new Rect((int)(ra.Width * 0.7), (int)(ra.Height * 0.35), (int)(ra.Width * 0.2), (int)(ra.Height * 0.15)) }); - IStringLocalizer stringLocalizer = App.GetService>() ?? throw new NullReferenceException(nameof(stringLocalizer)); - CultureInfo cultureInfo = new CultureInfo(TaskContext.Instance().Config.OtherConfig.GameCultureInfoName); - string shopOff = stringLocalizer.WithCultureGet(cultureInfo, "已售"); + string shopOff = "已售"; var shopOffRo = list.FirstOrDefault(r => r.Text.Contains(shopOff)); if (shopOffRo != null) { @@ -419,7 +421,28 @@ internal class GoToSereniteaPotTask { Logger.LogInformation("领取尘歌壶奖励:{text}", "领取好感和宝钱"); await Delay(1000, ct); - CaptureToRectArea().Find(ElementAssets.Instance.SereniteaPotLoveRo, a => a.Click()); + + var getAare = CaptureToRectArea(); + var count = OcrFactory.Paddle.OcrWithoutDetector(getAare.DeriveCrop(getAare.Width* 1801 / 1920, + getAare.Height* 609 / 1080,getAare.Width * 75 / 1920,getAare.Width * 46 / 1920).SrcMat); + + var match = System.Text.RegularExpressions.Regex.Match(count, @"(\d+)\s*[/17]\s*(8)"); + var shouldClick = true; + if (match.Success) + { + var numericPart = StringUtils.TryParseInt(match.Groups[1].Value); + if (numericPart == 0) + { + Logger.LogWarning("领取尘歌壶奖励:{text}", "没有角色可领取好感"); //存好感 + shouldClick = false; + } + } + + if (shouldClick) + { + getAare.Find(ElementAssets.Instance.SereniteaPotLoveRo, a => a.Click()); + } + await Delay(500, ct); var ra = CaptureToRectArea(); var list = ra.FindMulti(new RecognitionObject @@ -603,34 +626,6 @@ internal class GoToSereniteaPotTask await tp.Tp(4508.97509765625, 3630.557373046875); // TP到枫丹 } - private async Task ChangeCountryForce(string country, CancellationToken ct) - { - GameCaptureRegion.GameRegionClick((rect, scale) => (rect.Width - 160 * scale, rect.Height - 60 * scale)); - await Delay(500, ct); - using var ra = CaptureToRectArea(); - var list = ra.FindMulti(new RecognitionObject - { - RecognitionType = RecognitionTypes.Ocr, - RegionOfInterest = new Rect(ra.Width / 2, 0, ra.Width / 2, ra.Height) - }); - IStringLocalizer stringLocalizer = App.GetService>() ?? throw new NullReferenceException(nameof(stringLocalizer)); - CultureInfo cultureInfo = new CultureInfo(TaskContext.Instance().Config.OtherConfig.GameCultureInfoName); - string minCountryLocalized = stringLocalizer.WithCultureGet(cultureInfo, country); - string commissionLocalized = stringLocalizer.WithCultureGet(cultureInfo, "委托"); - Region? matchRect = list.FirstOrDefault(r => r.Text.Length == minCountryLocalized.Length && !r.Text.Contains(commissionLocalized) && r.Text.Contains(minCountryLocalized)); - if (matchRect == null) - { - Logger.LogWarning("切换区域失败:{Country}", country); - } - else - { - matchRect.Click(); - Logger.LogInformation("切换到区域:{Country}", country); - } - - await Delay(500, ct); - } - public async Task DoOnce(CancellationToken ct) { InitConfigList(); diff --git a/BetterGenshinImpact/GameTask/Common/Job/SetTimeTask.cs b/BetterGenshinImpact/GameTask/Common/Job/SetTimeTask.cs index 6e7d37a0..49fc3e61 100644 --- a/BetterGenshinImpact/GameTask/Common/Job/SetTimeTask.cs +++ b/BetterGenshinImpact/GameTask/Common/Job/SetTimeTask.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using BetterGenshinImpact.Core.Simulator; using BetterGenshinImpact.GameTask.AutoSkip.Assets; +using BetterGenshinImpact.GameTask.Common.BgiVision; using BetterGenshinImpact.GameTask.Common.Element.Assets; using BetterGenshinImpact.GameTask.Model.Area; using BetterGenshinImpact.View.Drawable; @@ -66,14 +67,16 @@ public class SetTimeTask GameCaptureRegion.GameRegion1080PPosClick(45, 715); await Delay(400, ct); await _returnMainUiTask.Start(ct); + // 跳过动画不总能成功 + if (Bv.IsInMainUi(CaptureToRectArea())) + { + return; + } } - else - { - await Delay(3000, ct); - // 出现X的时候代表时间切换成功 - await NewRetry.WaitForAction(() => CaptureToRectArea().Find(ElementAssets.Instance.PageCloseWhiteRo).IsExist(), ct, 25); - await _returnMainUiTask.Start(ct); - } + await Delay(3000, ct); + // 出现X的时候代表时间切换成功 + await NewRetry.WaitForAction(() => CaptureToRectArea().Find(ElementAssets.Instance.PageCloseWhiteRo).IsExist(), ct, 25); + await _returnMainUiTask.Start(ct); } // 取消动画函数 diff --git a/BetterGenshinImpact/GameTask/Common/Job/SwitchPartyTask.cs b/BetterGenshinImpact/GameTask/Common/Job/SwitchPartyTask.cs index 77c00fa1..cbaa2881 100644 --- a/BetterGenshinImpact/GameTask/Common/Job/SwitchPartyTask.cs +++ b/BetterGenshinImpact/GameTask/Common/Job/SwitchPartyTask.cs @@ -1,19 +1,20 @@ -using BetterGenshinImpact.Core.Recognition; +using BetterGenshinImpact.Core.Recognition; using BetterGenshinImpact.Core.Simulator; using BetterGenshinImpact.Core.Simulator.Extensions; using BetterGenshinImpact.GameTask.Common.BgiVision; using BetterGenshinImpact.GameTask.Common.Element.Assets; +using BetterGenshinImpact.GameTask.Common.Exceptions; using BetterGenshinImpact.GameTask.Model.Area; +using BetterGenshinImpact.View.Drawable; using Microsoft.Extensions.Logging; using OpenCvSharp; using System; using System.Linq; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; -using BetterGenshinImpact.GameTask.Common.Exceptions; using Vanara.PInvoke; using static BetterGenshinImpact.GameTask.Common.TaskControl; -using System.Text.RegularExpressions; namespace BetterGenshinImpact.GameTask.Common.Job; @@ -28,10 +29,10 @@ public class SwitchPartyTask public async Task Start(string partyName, CancellationToken ct) { bool isInPartyViewUi = false; - + Logger.LogInformation("尝试切换至队伍: {Name}", partyName); using var ra1 = CaptureToRectArea(); - + if (!Bv.IsInPartyViewUi(ra1)) { isInPartyViewUi = true; @@ -55,7 +56,7 @@ public class SwitchPartyTask Simulation.SendInput.SimulateAction(GIActions.OpenPartySetupScreen); // 考虑加载时间 2s,共检查 4.2s,如果失败则抛出异常 - + for (int i = 0; i < 7; i++) // 检查 7 次 { await Delay(600, ct); @@ -102,7 +103,7 @@ public class SwitchPartyTask await Delay(500, ct); await _returnMainUiTask.Start(ct); } - + return true; } @@ -135,9 +136,6 @@ public class SwitchPartyTask } } - var nextX = partyDeleteBtn.Left; - var nextY = partyDeleteBtn.Top - partyDeleteBtn.Height * 2; - // 点击到最上方 await Task.Delay(50, ct); GameCaptureRegion.GameRegion1080PPosClick(700, 120); @@ -146,26 +144,68 @@ public class SwitchPartyTask await Task.Delay(450, ct); Simulation.SendInput.Mouse.LeftButtonUp(); - // 逐页查找 - for (var i = 0; i < 11; i++) + Rect regionOfInterest = new Rect(0, (int)(80 * _assetScale), partyDeleteBtn.Right, partyDeleteBtn.Top - (int)(80 * _assetScale)); + RecognitionObject recognitionObject = new RecognitionObject { - using var page = CaptureToRectArea(); - var found = await FindPage(partyName, page, partyDeleteBtn, ct, isInPartyViewUi); - if (found) + RecognitionType = RecognitionTypes.Ocr, + RegionOfInterest = regionOfInterest, + DrawOnWindow = true, + Name = "队伍名称", + DrawOnWindowPen= new System.Drawing.Pen(System.Drawing.Color.White) + }; + // 逐页查找 + try + { + for (var i = 0; i < 16; i++) // 6.0版本最多20个队伍 { - RunnerContext.Instance.ClearCombatScenes(); - return true; - } + using var page = CaptureToRectArea(); - // 点击下一页 - if (i == 0) - { - // #ebe4d8 首次点一下第一个,防止第五个被点击过 - page.ClickTo(600 * _assetScale, 200 * _assetScale); - } + var partySwitchNameRaList = page.FindMulti(recognitionObject); - page.ClickTo(nextX, nextY); // 点击最下方队伍下移 - await Delay(400, ct); + if (partySwitchNameRaList == null || partySwitchNameRaList.Count <= 0) + { + Logger.LogInformation("管理队伍界面文字识别失败"); + break; + } + + // 当前页存在则直接点击 + foreach (var textRegion in partySwitchNameRaList) + { + if (Regex.IsMatch(textRegion.Text, partyName)) + { + page.ClickTo(textRegion.Right + textRegion.Width, textRegion.Bottom); + await Delay(200, ct); + Logger.LogInformation("切换队伍成功: {Text}", textRegion.Text); + await ConfirmParty(page, ct, isInPartyViewUi); + + RunnerContext.Instance.ClearCombatScenes(); + return true; + } + } + + Region lowest = partySwitchNameRaList.Where(r => r.X > 35 * _assetScale && r.X < 100 * _assetScale).OrderBy(r => r.Y).Last(); + lowest.DrawSelf("底部的队伍"); + + if (lowest.Y < 777 * _assetScale) // 如果最底下是空队伍则不会有队伍名,以此判断是否已遍历完成 + { + Logger.LogInformation("已抵达最后一个队伍"); + break; + } + + // 点击下一页 + if (i == 0) + { + // #ebe4d8 首次点一下第一个,防止第五个被点击过 + page.ClickTo(600 * _assetScale, 200 * _assetScale); + } + + page.ClickTo(regionOfInterest.X + regionOfInterest.Width / 2, lowest.Bottom); // 点击最下方队伍下移 + await Delay(400, ct); + } + } + finally + { + VisionContext.Instance().DrawContent.ClearAll(); } // 未找到 @@ -175,30 +215,6 @@ public class SwitchPartyTask return false; } - private async Task FindPage(string partyName, ImageRegion page, Region partyDeleteBtn, CancellationToken ct, bool isInPartyViewUi = false) - { - var partySwitchNameRaList = page.FindMulti(new RecognitionObject - { - RecognitionType = RecognitionTypes.Ocr, - RegionOfInterest = new Rect(0, (int)(80 * _assetScale), partyDeleteBtn.Right, partyDeleteBtn.Top - (int)(80 * _assetScale)) - }); - - // 当前页存在则直接点击 - foreach (var textRegion in partySwitchNameRaList) - { - if (Regex.IsMatch(textRegion.Text, partyName)) - { - page.ClickTo(textRegion.Right + textRegion.Width, textRegion.Bottom); - await Delay(200, ct); - Logger.LogInformation("切换队伍成功: {Text}", textRegion.Text); - await ConfirmParty(page, ct, isInPartyViewUi); - return true; - } - } - - return false; - } - private async Task ConfirmParty(ImageRegion page, CancellationToken ct, bool isInPartyViewUi = false) { var r1 = Bv.ClickWhiteConfirmButton(page.DeriveCrop(0, page.Height / 4, page.Width / 4, page.Height - page.Height / 4)); @@ -215,6 +231,6 @@ public class SwitchPartyTask using var ra = CaptureToRectArea(); var r2 = Bv.ClickWhiteConfirmButton(ra.DeriveCrop(page.Width - page.Width / 4, page.Height / 4, page.Width / 4, page.Height - page.Height / 4)); await Delay(500, ct); - if (isInPartyViewUi)await _returnMainUiTask.Start(ct); + if (isInPartyViewUi) await _returnMainUiTask.Start(ct); } } diff --git a/BetterGenshinImpact/GameTask/GameLoading/GameLoading.cs b/BetterGenshinImpact/GameTask/GameLoading/GameLoading.cs index 6eca98a2..5af58439 100644 --- a/BetterGenshinImpact/GameTask/GameLoading/GameLoading.cs +++ b/BetterGenshinImpact/GameTask/GameLoading/GameLoading.cs @@ -19,9 +19,11 @@ namespace BetterGenshinImpact.GameTask.GameLoading; public class GameLoadingTrigger : ITaskTrigger { + public static bool GlobalEnabled = true; + public string Name => "自动开门"; - public bool IsEnabled { get; set; } + public bool IsEnabled { get => GlobalEnabled; set {} } public int Priority => 999; @@ -61,9 +63,17 @@ public class GameLoadingTrigger : ITaskTrigger _assets = GameLoadingAssets.Instance; } + public void InnerSetEnabled(bool enabled) + { + GlobalEnabled = enabled; + } + public void Init() { - IsEnabled = _config.AutoEnterGameEnabled; + if (!_config.AutoEnterGameEnabled) + { + InnerSetEnabled(false); + } // // 前面没有联动启动原神,这个任务也不用启动 // if ((DateTime.Now - TaskContext.Instance().LinkedStartGenshinTime).TotalMinutes >= 5) @@ -234,14 +244,14 @@ public class GameLoadingTrigger : ITaskTrigger // 5min 后自动停止 if ((DateTime.Now - _triggerStartTime).TotalMinutes >= 5) { - IsEnabled = false; + InnerSetEnabled(false); return; } // 成功进入游戏判断 if (Bv.IsInMainUi(content.CaptureRectArea) || Bv.IsInAnyClosableUi(content.CaptureRectArea) || Bv.IsInDomain(content.CaptureRectArea)) { - _logger.LogInformation("已进入游戏"); - IsEnabled = false; + // _logger.LogInformation("当前在游戏主界面"); + InnerSetEnabled(false); return; } diff --git a/BetterGenshinImpact/GameTask/GetGridIcons/GridIconsAccuracyTestTask.cs b/BetterGenshinImpact/GameTask/GetGridIcons/GridIconsAccuracyTestTask.cs index 63101ddd..4e5a9e22 100644 --- a/BetterGenshinImpact/GameTask/GetGridIcons/GridIconsAccuracyTestTask.cs +++ b/BetterGenshinImpact/GameTask/GetGridIcons/GridIconsAccuracyTestTask.cs @@ -16,6 +16,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using BetterGenshinImpact.Core.Config; using static BetterGenshinImpact.GameTask.Common.TaskControl; namespace BetterGenshinImpact.GameTask.GetGridIcons; @@ -51,7 +52,7 @@ public class GridIconsAccuracyTestTask : ISoloTask public static InferenceSession LoadModel(out Dictionary prototypes) { #region 加载model - var session = new InferenceSession(@".\Assets\Model\Item\gridIcon.onnx"); + var session = new InferenceSession(Global.Absolute(@"Assets\Model\Item\gridIcon.onnx")); var metadata = session.ModelMetadata; @@ -62,7 +63,7 @@ public class GridIconsAccuracyTestTask : ISoloTask List prefixList = System.Text.Json.JsonSerializer.Deserialize>(prefixListJson) ?? throw new Exception(); // 不预测前缀 #endregion #region 加载原型向量 - var allLines = File.ReadLines(@".\Assets\Model\Item\items.csv").Skip(1); // 跳过首行列名 + var allLines = File.ReadLines(Global.Absolute(@"Assets\Model\Item\items.csv")).Skip(1); // 跳过首行列名 prototypes = new Dictionary(); foreach (string line in allLines) { @@ -113,15 +114,15 @@ public class GridIconsAccuracyTestTask : ISoloTask Task task1 = Delay(300, ct); // 用模型推理得到的结果 - Task<(string, int)> task2 = Task.Run(() => + Task<(string?, int)> task2 = Task.Run(() => { using Mat icon = itemRegion.SrcMat.GetGridIcon(); return Infer(icon, session, prototypes); }, ct); await Task.WhenAll(task1, task2); - (string, int) result = task2.Result; - string predName = result.Item1; + (string?, int) result = task2.Result; + string? predName = result.Item1; int predStarNum = result.Item2; // 用CV方法得到的结果 @@ -135,7 +136,11 @@ public class GridIconsAccuracyTestTask : ISoloTask // 统计结果 total_count++; - if (itemName.Contains(predName) && predStarNum == itemStarNum) + if (predName == null) + { + logger.LogInformation($"模型没有识别,应为:{itemName}|{itemStarNum}星,❌,正确率{total_acc / total_count:0.00}"); + } + else if (itemName.Contains(predName) && predStarNum == itemStarNum) { total_acc++; logger.LogInformation($"{predName}|{predStarNum}星,✔,正确率{total_acc / total_count:0.00}"); @@ -162,7 +167,7 @@ public class GridIconsAccuracyTestTask : ISoloTask /// /// (预测名称, 预测星级) /// - public static (string, int) Infer(Mat mat, InferenceSession session, Dictionary prototypes) + public static (string?, int) Infer(Mat mat, InferenceSession session, Dictionary prototypes) { if (mat.Size().Width != 125 || mat.Size().Height != 125) { @@ -193,15 +198,18 @@ public class GridIconsAccuracyTestTask : ISoloTask } if (min2 == null || distance2 < min2) { - pred_name = prototype.Key; min2 = distance2; + if (min2 < 10 * 10) // todo:负样本距离10直接读取模型 + { + pred_name = prototype.Key; + } } } - if (pred_name == null || min2 == null) + if (min2 == null) { throw new Exception("特征数据为空"); } - min2 = Math.Sqrt(min2.Value); + // min2 = Math.Sqrt(min2.Value); int pred_star = results[2].AsEnumerable().ToList().IndexOf(results[2].AsEnumerable().Max()); return (pred_name, pred_star); } diff --git a/BetterGenshinImpact/GameTask/TaskTriggerDispatcher.cs b/BetterGenshinImpact/GameTask/TaskTriggerDispatcher.cs index bdab1edf..a451643d 100644 --- a/BetterGenshinImpact/GameTask/TaskTriggerDispatcher.cs +++ b/BetterGenshinImpact/GameTask/TaskTriggerDispatcher.cs @@ -12,6 +12,7 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; +using BetterGenshinImpact.GameTask.GameLoading; using Fischless.GameCapture.Graphics; using Vanara.PInvoke; @@ -116,6 +117,7 @@ namespace BetterGenshinImpact.GameTask // 初始化触发器(一定要在任务上下文初始化完毕后使用) _triggers = GameTaskManager.LoadInitialTriggers(); + GameLoadingTrigger.GlobalEnabled = TaskContext.Instance().Config.GenshinStartConfig.AutoEnterGameEnabled; // if (GraphicsCapture.IsHdrEnabled(hWnd)) // { diff --git a/BetterGenshinImpact/Helpers/CultureHelper.cs b/BetterGenshinImpact/Helpers/CultureHelper.cs index a0b24f59..cfcc5c6f 100644 --- a/BetterGenshinImpact/Helpers/CultureHelper.cs +++ b/BetterGenshinImpact/Helpers/CultureHelper.cs @@ -1,11 +1,9 @@ -using BetterGenshinImpact.GameTask.Common.BgiVision; using Microsoft.Extensions.Localization; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Text; -using System.Text.RegularExpressions; namespace BetterGenshinImpact.Helpers; public static class CultureHelper diff --git a/BetterGenshinImpact/Helpers/Win32/ConsoleHelper.cs b/BetterGenshinImpact/Helpers/Win32/ConsoleHelper.cs index e1a2aa3f..d120a1f1 100644 --- a/BetterGenshinImpact/Helpers/Win32/ConsoleHelper.cs +++ b/BetterGenshinImpact/Helpers/Win32/ConsoleHelper.cs @@ -89,19 +89,19 @@ public static class ConsoleHelper // 重定向标准输出流 var stdOutHandle = GetStdHandle(STD_OUTPUT_HANDLE); var stdOutStream = new FileStream(stdOutHandle, FileAccess.Write); - var stdOutWriter = new StreamWriter(stdOutStream, Encoding.UTF8) { AutoFlush = true }; + var stdOutWriter = new StreamWriter(stdOutStream, Console.OutputEncoding) { AutoFlush = true }; Console.SetOut(stdOutWriter); // 重定向标准错误流 var stdErrHandle = GetStdHandle(STD_ERROR_HANDLE); var stdErrStream = new FileStream(stdErrHandle, FileAccess.Write); - var stdErrWriter = new StreamWriter(stdErrStream, Encoding.UTF8) { AutoFlush = true }; + var stdErrWriter = new StreamWriter(stdErrStream, Console.OutputEncoding) { AutoFlush = true }; Console.SetError(stdErrWriter); // 重定向标准输入流 var stdInHandle = GetStdHandle(STD_INPUT_HANDLE); var stdInStream = new FileStream(stdInHandle, FileAccess.Read); - var stdInReader = new StreamReader(stdInStream, Encoding.UTF8); + var stdInReader = new StreamReader(stdInStream, Console.InputEncoding); Console.SetIn(stdInReader); } diff --git a/BetterGenshinImpact/Model/ConditionDefinition.cs b/BetterGenshinImpact/Model/ConditionDefinition.cs index 67d0feb1..5a33fd14 100644 --- a/BetterGenshinImpact/Model/ConditionDefinition.cs +++ b/BetterGenshinImpact/Model/ConditionDefinition.cs @@ -119,7 +119,7 @@ public class ConditionDefinitions { Subject = "动作", Description = "路线中含有特殊动作时使用的队伍名称,优先级高于采集物配置。队伍名称是你在游戏中手动设置的队伍名称文字。队伍中,必须存在和动作相关的角色。程序会自动识别并使用对应的角色执行动作,具体见文档", - ObjectOptions = new List { "纳西妲采集", "水元素采集", "雷元素采集", "风元素采集" }, + ObjectOptions = new List { "纳西妲采集", "水元素采集", "雷元素采集", "风元素采集", "火元素采集" }, } }, { @@ -145,5 +145,6 @@ public class ConditionDefinitions { "hydro_collect", "水元素采集" }, { "electro_collect", "雷元素采集" }, { "anemo_collect", "风元素采集" }, + { "pyro_collect", "火元素采集" }, }; } diff --git a/BetterGenshinImpact/Resources/Images/Anniversary/logo_1st.ico b/BetterGenshinImpact/Resources/Images/Anniversary/logo_1st.ico deleted file mode 100644 index 3a42641d..00000000 Binary files a/BetterGenshinImpact/Resources/Images/Anniversary/logo_1st.ico and /dev/null differ diff --git a/BetterGenshinImpact/Resources/Images/Anniversary/logo_1st.png b/BetterGenshinImpact/Resources/Images/Anniversary/logo_1st.png deleted file mode 100644 index e0b479e7..00000000 Binary files a/BetterGenshinImpact/Resources/Images/Anniversary/logo_1st.png and /dev/null differ diff --git a/BetterGenshinImpact/Resources/Images/Anniversary/logo_2nd.ico b/BetterGenshinImpact/Resources/Images/Anniversary/logo_2nd.ico deleted file mode 100644 index dd741cef..00000000 Binary files a/BetterGenshinImpact/Resources/Images/Anniversary/logo_2nd.ico and /dev/null differ diff --git a/BetterGenshinImpact/Resources/Images/Anniversary/logo_2nd.png b/BetterGenshinImpact/Resources/Images/Anniversary/logo_2nd.png deleted file mode 100644 index 052f17be..00000000 Binary files a/BetterGenshinImpact/Resources/Images/Anniversary/logo_2nd.png and /dev/null differ diff --git a/BetterGenshinImpact/Resources/Images/Anniversary/logo_3rd.ico b/BetterGenshinImpact/Resources/Images/Anniversary/logo_3rd.ico deleted file mode 100644 index d107843b..00000000 Binary files a/BetterGenshinImpact/Resources/Images/Anniversary/logo_3rd.ico and /dev/null differ diff --git a/BetterGenshinImpact/Resources/Images/Anniversary/logo_3rd.png b/BetterGenshinImpact/Resources/Images/Anniversary/logo_3rd.png deleted file mode 100644 index 3786c463..00000000 Binary files a/BetterGenshinImpact/Resources/Images/Anniversary/logo_3rd.png and /dev/null differ diff --git a/BetterGenshinImpact/Resources/Images/logo.ico b/BetterGenshinImpact/Resources/Images/logo.ico index 7e25997c..35439042 100644 Binary files a/BetterGenshinImpact/Resources/Images/logo.ico and b/BetterGenshinImpact/Resources/Images/logo.ico differ diff --git a/BetterGenshinImpact/Resources/Images/logo.png b/BetterGenshinImpact/Resources/Images/logo.png index 1390a0ba..b8f3957f 100644 Binary files a/BetterGenshinImpact/Resources/Images/logo.png and b/BetterGenshinImpact/Resources/Images/logo.png differ diff --git a/BetterGenshinImpact/Service/ScriptService.cs b/BetterGenshinImpact/Service/ScriptService.cs index 8476b8ce..c8986dc0 100644 --- a/BetterGenshinImpact/Service/ScriptService.cs +++ b/BetterGenshinImpact/Service/ScriptService.cs @@ -429,7 +429,10 @@ public partial class ScriptService : IScriptService if (!fisrt&&!RunnerContext.Instance.IsPreExecution) { - Notify.Event(NotificationEvent.GroupEnd).Success($"配置组{groupName}结束"); + if (CancellationContext.Instance.IsManualStop is false) + { + Notify.Event(NotificationEvent.GroupEnd).Success($"配置组{groupName}结束"); + } } if (taskProgress != null) @@ -611,7 +614,6 @@ public partial class ScriptService : IScriptService GlobalMethod.MoveMouseTo(300, 300); } - } } }); diff --git a/BetterGenshinImpact/User/AutoFight/万能战斗策略(萌新推荐).txt b/BetterGenshinImpact/User/AutoFight/万能战斗策略(萌新推荐).txt new file mode 100644 index 00000000..c913e5c2 --- /dev/null +++ b/BetterGenshinImpact/User/AutoFight/万能战斗策略(萌新推荐).txt @@ -0,0 +1,54 @@ +// 作者:火山 +// 描述:万能战斗策略(新手推荐)。需要在,调度器→配置组→设置→战斗配置(开启)→自动检测战斗结束(开启)→更快检查结束战斗(开启)→旋转寻找敌人位置(开启)→检查战斗结束的延时(0.1)→按键触发后检查延时(0.35)→盾奶角色优先释放技能(开启,设置你的盾位)→战斗结束后使用万叶长E手机掉落物(开启) + +// 盾(刚需) +茜特菈莉 attack,e,wait(0.2),keypress(q),wait(0.2),keypress(q),wait(0.2),keypress(q),attack(0.2) +伊涅芙 e,attack(0.22),keypress(q),wait(0.1),keypress(q),attack(0.2),keypress(q),attack(0.2) +钟离 s(0.2), e(hold), wait(0.2), w(0.2),keypress(q),wait(0.2),keypress(q),attack(0.1) +莱依拉 e,wait(0.2), keypress(q),wait(0.2),keypress(q),attack(0.2),keypress(q),attack(0.2) +绮良良 e,attack(0.2), keypress(q),attack(0.2),keypress(q),wait(0.2),keypress(q),attack(0.2) +托马 e,attack(0.22),keypress(q),wait(0.1),keypress(q),attack(0.2),keypress(q),attack(0.2) +蓝砚 e,attack(0.15), click(middle),attack(0.15),click(middle),attack(0.15),click(middle),wait(0.2).dash(0.1),attack(0.2) + +// 后台、挂元素、副C、先手 +玛薇卡 attack(0.2),e +迪希雅 e,attack(0.2),e +香菱 e,wait(0.3),keypress(q),attack(0.2),keypress(q),wait(0.2),keypress(q),attack(0.2) +仆人 attack,e +那维莱特 attack(0.23),e +纳西妲 e(hold),click(middle),keypress(q),wait(0.3),keypress(q),attack(0.3),keypress(q),attack(0.2) +艾梅莉埃 e,attack(0.2), keypress(q),attack(0.2),keypress(q),wait(0.2),keypress(q),attack(0.2) +丝柯克 attack(0.2),click(middle),keypress(q),wait(0.05),keypress(q),attack(0.05),click(middle),keydown(E),wait(0.22),attack(0.08),click(middle),keyup(E),keypress(q),wait(0.08),keypress(q) +芙宁娜 e,attack(0.2), keypress(q),attack(0.2),keypress(q),wait(0.2),keypress(q),attack(0.2) +白术 e,attack(0.2) +芭芭拉 e,attack(0.2) +希格雯 e(hold),wait(0.2),keypress(q),wait(0.2),keypress(q) +爱可菲 e,attack(0.2), keypress(q),attack(0.2),keypress(q),wait(0.2),keypress(q),attack(0.2) +菲谢尔 e +欧洛伦 e,attack(0.3), keypress(q),wait(0.2),attack(0.3),keypress(q),wait(0.2),attack(0.3),keypress(q),wait(0.3) +雷电将军 e,attack(0.22),keypress(q),wait(0.1),keypress(q),attack(0.2),keypress(q),attack(0.2) +久岐忍 e,wait(0.2),keypress(q),attack(0.15),keypress(q),e +瓦雷莎 e, attack(1.25),wait(0.45), s(0.4), click(middle), e, attack(1.25), wait(0.3),keypress(q), wait(0.45) + + +// 中置位 +夏沃蕾 attack(0.08),keypress(q),wait(0.2),keypress(q),wait(0.2),attack(0.2),keydown(e),wait(0.15), moveby(0,900),wait(0.15),keyup(e),attack(0.15) +白术 attack(0.2), keypress(q),attack(0.2),keypress(q),wait(0.2),keypress(q),attack(0.2) +那维莱特 attack(0.08),keypress(q),wait(0.22),keypress(q),wait(0.2),keypress(q),e + + +//减抗 +希诺宁 s(0.2),e,w(0.2),attack(0.35),wait(0.1),attack(0.35),keypress(x), wait(0.2), keypress(q), wait(0.3), keypress(q),keypress(x), wait(0.08), keypress(x),attack(0.2) +枫原万叶 attack(0.08),keypress(q),wait(0.3),keypress(q),wait(0.3),attack(0.2),keydown(E),wait(0.48),keyup(E),attack(0.3), wait(0.5),attack(0.1) +砂糖 e,attack(0.2),keypress(q),attack(0.2),keypress(q),e,attack(0.2) + +//爆发 +玛薇卡 e,click(middle),wait(0.12), keypress(q), wait(0.3), keypress(q), wait(0.3), charge(3.8), keydown(space), wait(0.1), keyup(space), attack(0.2),wait(0.2) + +//收尾,长轴 +那维莱特 charge(3),j,wait(0.3) +丝柯克 attack(0.05),keypress(e),wait(0.05),keypress(e),wait(0.2),attack(2.27),keypress(Q),dash,attack(2.27),keydown(S),keypress(Q),dash,keyup(S),attack(2.27),wait(0.11),charge(0.3),attack(1) +迪希雅 keypress(q),attack(0.1),dash(0.2),keypress(q),attack(0.3),keypress(q),attack(0.3),keypress(q),attack(0.3),keydown(S),attack(0.5),keyup(S),keydown(W),attack(0.5),keyup(W),keydown(S),attack(0.5),keyup(S),keydown(W),attack(0.5),keyup(W),keydown(S),attack(0.5),keyup(S),keydown(W),attack(0.5),keyup(W),keydown(S),attack(0.5),keyup(S) +娜维娅 keypress(q),attack(0.1),keypress(q),attack(0.1),keypress(q),attack(0.1),keypress(q),keydown(E),wait(0.8),keyup(E),attack(1.6),keydown(E),wait(0.8),keyup(E),attack(0.1),keydown(S),attack(0.33),keyup(S),keydown(W),attack(0.3),keyup(W),keydown(S),attack(0.3),keyup(S),keydown(W),attack(0.3),keyup(W),keydown(S),attack(0.3),keyup(S),keydown(W),attack(0.3),keyup(W),attack(0.2) +瓦雷莎 e, attack(1.25),wait(0.45), s(0.4), e, attack(1.25), wait(0.3),keypress(q), wait(0.45),s(0.4),e, attack(1.25),wait(0.45), s(0.4), e, attack(1.25), wait(0.3),keypress(q), attack(0.45) +仆人 charge(0.35),j,attack(3.2),j,attack(0.3),attack(0.52),keypress(q),attack(0.2) \ No newline at end of file diff --git a/BetterGenshinImpact/View/Pages/ScriptControlPage.xaml b/BetterGenshinImpact/View/Pages/ScriptControlPage.xaml index 5c89eeb0..e35fda0d 100644 --- a/BetterGenshinImpact/View/Pages/ScriptControlPage.xaml +++ b/BetterGenshinImpact/View/Pages/ScriptControlPage.xaml @@ -358,6 +358,22 @@ + + + + + diff --git a/BetterGenshinImpact/View/Windows/CheckUpdateWindow.xaml b/BetterGenshinImpact/View/Windows/CheckUpdateWindow.xaml index 1ebde5f5..12a55c25 100644 --- a/BetterGenshinImpact/View/Windows/CheckUpdateWindow.xaml +++ b/BetterGenshinImpact/View/Windows/CheckUpdateWindow.xaml @@ -69,7 +69,6 @@ @@ -92,7 +91,7 @@ @@ -121,7 +120,7 @@ diff --git a/BetterGenshinImpact/View/Windows/CheckUpdateWindow.xaml.cs b/BetterGenshinImpact/View/Windows/CheckUpdateWindow.xaml.cs index bc23e0d8..f38297c4 100644 --- a/BetterGenshinImpact/View/Windows/CheckUpdateWindow.xaml.cs +++ b/BetterGenshinImpact/View/Windows/CheckUpdateWindow.xaml.cs @@ -14,6 +14,7 @@ using BetterGenshinImpact.Model; using Meziantou.Framework.Win32; using Wpf.Ui.Controls; using Wpf.Ui.Violeta.Controls; +using MessageBoxResult = System.Windows.MessageBoxResult; namespace BetterGenshinImpact.View.Windows; @@ -30,7 +31,7 @@ public partial class CheckUpdateWindow : FluentWindow [ObservableProperty] private string selectedGitSource = "Github"; - public string GitSourceDescription => SelectedGitSource == "CNB" ? "直接从 CNB 下载并更新" : "直接从 Github 下载并更新"; + public string GitSourceDescription => SelectedGitSource == "CNB" ? "【国内】直接从 CNB 下载并更新" : "【国外】直接从 Github 下载并更新"; partial void OnSelectedGitSourceChanged(string value) { @@ -44,13 +45,13 @@ public partial class CheckUpdateWindow : FluentWindow _option = option ?? throw new ArgumentNullException(nameof(option)); DataContext = this; InitializeComponent(); - + // 存在CDK则显示修改按钮 if (string.IsNullOrEmpty(MirrorChyanHelper.GetCdk())) { EditCdkButton.Visibility = Visibility.Collapsed; } - + if (option.Trigger == UpdateTrigger.Manual) { IgnoreButton.Visibility = Visibility.Collapsed; @@ -62,11 +63,11 @@ public partial class CheckUpdateWindow : FluentWindow WebpagePanel.Visibility = Visibility.Collapsed; UpdateStatusMessageGrid.Height = 0; ShowUpdateStatus = false; - + // 隐藏开源渠道和Steambird服务卡片 GitSourceCard.Visibility = Visibility.Collapsed; SteambirdCard.Visibility = Visibility.Collapsed; - + // // 删除前几行 // MyGrid.RowDefinitions.RemoveAt(0); // MyGrid.RowDefinitions.RemoveAt(0); @@ -91,7 +92,7 @@ public partial class CheckUpdateWindow : FluentWindow Closing += OnClosing; - + // 延迟显示气泡提示 if (option.Channel != UpdateChannel.Alpha) { @@ -103,7 +104,7 @@ public partial class CheckUpdateWindow : FluentWindow { showTimer.Stop(); ShowOtherUpdateTip = true; - + // 5秒后自动消失 var hideTimer = new DispatcherTimer { @@ -118,7 +119,6 @@ public partial class CheckUpdateWindow : FluentWindow }; showTimer.Start(); } - } protected void OnClosing(object? sender, CancelEventArgs e) @@ -165,9 +165,21 @@ public partial class CheckUpdateWindow : FluentWindow private async Task UpdateFromGitHostPlatformAsync() { string source = SelectedGitSource == "CNB" ? "cnb" : "github"; + + if (source == "github") + { + // 提示用户这个是国外服务器,可能会很慢 + var result = await MessageBox.ShowAsync("您已选择「Github」作为更新源。\n请确认:您当前网络可正常访问 Github 文件服务?\n若不确定能否访问,建议切换至其他更新渠道。\n是否继续使用 Github 渠道更新?", + "警告", System.Windows.MessageBoxButton.OKCancel, MessageBoxImage.Exclamation, System.Windows.MessageBoxResult.None); + if (result != MessageBoxResult.OK) + { + return; + } + } + await RunUpdaterAsync($"-I --source {source}"); } - + [RelayCommand] private async Task UpdateFromSteambirdAsync() @@ -183,7 +195,7 @@ public partial class CheckUpdateWindow : FluentWindow { return; } - + if (_option.Channel == UpdateChannel.Stable) { await RunUpdaterAsync("-I --source mirrorc"); @@ -234,7 +246,7 @@ public partial class CheckUpdateWindow : FluentWindow await UserInteraction.Invoke(this, CheckUpdateWindowButton.Cancel); } } - + [RelayCommand] private void EditCdk() { diff --git a/BetterGenshinImpact/ViewModel/Pages/HotKeyPageViewModel.cs b/BetterGenshinImpact/ViewModel/Pages/HotKeyPageViewModel.cs index 7ed038bd..7cf2ab22 100644 --- a/BetterGenshinImpact/ViewModel/Pages/HotKeyPageViewModel.cs +++ b/BetterGenshinImpact/ViewModel/Pages/HotKeyPageViewModel.cs @@ -218,7 +218,7 @@ public partial class HotKeyPageViewModel : ObservableObject, IViewModel nameof(Config.HotKeyConfig.CancelTaskHotkey), Config.HotKeyConfig.CancelTaskHotkey, Config.HotKeyConfig.CancelTaskHotkeyType, - (_, _) => { CancellationContext.Instance.Cancel(); } + (_, _) => { CancellationContext.Instance.ManualCancel(); } )); systemDirectory.Children.Add(new HotKeySettingModel( "暂停当前脚本/独立任务", diff --git a/BetterGenshinImpact/ViewModel/Pages/OneDragonFlowViewModel.cs b/BetterGenshinImpact/ViewModel/Pages/OneDragonFlowViewModel.cs index 47e2f46f..91ce8d79 100644 --- a/BetterGenshinImpact/ViewModel/Pages/OneDragonFlowViewModel.cs +++ b/BetterGenshinImpact/ViewModel/Pages/OneDragonFlowViewModel.cs @@ -665,7 +665,10 @@ public partial class OneDragonFlowViewModel : ViewModel if (CancellationContext.Instance.Cts.IsCancellationRequested) { _logger.LogInformation("任务被取消,退出执行"); - Notify.Event(NotificationEvent.DragonEnd).Success("一条龙和配置组任务结束"); + if (CancellationContext.Instance.IsManualStop is false) + { + Notify.Event(NotificationEvent.DragonEnd).Success("一条龙和配置组任务结束"); + } return; // 后续的检查任务也不执行 } } @@ -676,7 +679,10 @@ public partial class OneDragonFlowViewModel : ViewModel { await new CheckRewardsTask().Start(CancellationContext.Instance.Cts.Token); await Task.Delay(500); - Notify.Event(NotificationEvent.DragonEnd).Success("一条龙和配置组任务结束"); + if (CancellationContext.Instance.IsManualStop is false) + { + Notify.Event(NotificationEvent.DragonEnd).Success("一条龙和配置组任务结束"); + } _logger.LogInformation("一条龙和配置组任务结束"); // 执行完成后操作 diff --git a/BetterGenshinImpact/ViewModel/Pages/ScriptControlViewModel.cs b/BetterGenshinImpact/ViewModel/Pages/ScriptControlViewModel.cs index 09d696ca..e2d9aed7 100644 --- a/BetterGenshinImpact/ViewModel/Pages/ScriptControlViewModel.cs +++ b/BetterGenshinImpact/ViewModel/Pages/ScriptControlViewModel.cs @@ -1,16 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Collections.Specialized; -using System.ComponentModel; -using System.Diagnostics; -using System.Dynamic; -using System.IO; -using System.Linq; -using System.Text.Json; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; using BetterGenshinImpact.Core.Config; using BetterGenshinImpact.Core.Script; using BetterGenshinImpact.Core.Script.Group; @@ -32,15 +19,30 @@ using BetterGenshinImpact.ViewModel.Windows.Editable; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Diagnostics; +using System.Dynamic; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Threading; using Wpf.Ui; using Wpf.Ui.Controls; using Wpf.Ui.Violeta.Controls; -using StackPanel = Wpf.Ui.Controls.StackPanel; -using TextBox = Wpf.Ui.Controls.TextBox; using Button = Wpf.Ui.Controls.Button; using MessageBoxButton = System.Windows.MessageBoxButton; using MessageBoxResult = Wpf.Ui.Controls.MessageBoxResult; +using StackPanel = Wpf.Ui.Controls.StackPanel; using TextBlock = Wpf.Ui.Controls.TextBlock; +using TextBox = Wpf.Ui.Controls.TextBox; namespace BetterGenshinImpact.ViewModel.Pages; @@ -870,183 +872,302 @@ public partial class ScriptControlViewModel : ViewModel } [RelayCommand] - private void OnAddPathing() + private async Task OnAddPathing() { - var root = FileTreeNodeHelper.LoadDirectory(MapPathingViewModel.PathJsonPath); - var stackPanel = CreatePathingScriptSelectionPanel(root.Children); - - var result = PromptDialog.Prompt("请选择需要添加的地图追踪任务", "请选择需要添加的地图追踪任务", stackPanel, new Size(600, 720)); - if (!string.IsNullOrEmpty(result)) + try { - AddSelectedPathingScripts((StackPanel)stackPanel.Content); + // 在后台线程中加载数据 + var root = await Task.Run(() => FileTreeNodeHelper.LoadDirectory(MapPathingViewModel.PathJsonPath)); + + // 异步创建选择面板 + var stackPanel = await CreatePathingScriptSelectionPanelAsync(root.Children); + + // 显示选择对话框 + var result = PromptDialog.Prompt("请选择需要添加的地图追踪任务", "请选择需要添加的地图追踪任务", stackPanel, new Size(600, 720)); + + if (!string.IsNullOrEmpty(result)) + { + AddSelectedPathingScripts((StackPanel)stackPanel.Content); + } + } + catch (Exception ex) + { + Toast.Error($"加载地图追踪任务失败: {ex.Message}"); + _logger.LogError(ex, "加载地图追踪任务时发生错误"); } } - private ScrollViewer CreatePathingScriptSelectionPanel(IEnumerable> list) + // 添加防抖计时器字段 + private DispatcherTimer? _debounceTimer; + private const int DebounceDelayMs = 300; + + // 存储路径与UI元素的映射 + private readonly Dictionary _nodeUIElements = []; + + /// + /// 异步创建地图追踪任务选择面板 + /// + private async Task CreatePathingScriptSelectionPanelAsync(IEnumerable> list) { var stackPanel = new StackPanel(); - CheckBox excludeCheckBox = new CheckBox + CheckBox excludeCheckBox = new() { Content = "排除已选择过的目录", VerticalAlignment = VerticalAlignment.Center, }; - CheckBox deepCheckBox = new CheckBox + CheckBox deepCheckBox = new() { Content = "深度搜索", VerticalAlignment = VerticalAlignment.Center, }; - stackPanel.Children.Add(excludeCheckBox); - stackPanel.Children.Add(deepCheckBox); - - var filterTextBox = new TextBox + TextBox filterTextBox = new() { Margin = new Thickness(0, 0, 0, 10), PlaceholderText = "输入筛选条件...", }; - // 设置文本框自动聚焦 - filterTextBox.Loaded += (s, e) => filterTextBox.Focus(); - filterTextBox.TextChanged += delegate { ApplyFilter(stackPanel, list, filterTextBox.Text, excludeCheckBox.IsChecked, deepCheckBox.IsChecked); }; - excludeCheckBox.Click += delegate { ApplyFilter(stackPanel, list, filterTextBox.Text, excludeCheckBox.IsChecked, deepCheckBox.IsChecked); }; - deepCheckBox.Click += delegate { ApplyFilter(stackPanel, list, filterTextBox.Text, excludeCheckBox.IsChecked, deepCheckBox.IsChecked); }; + + // 初始化防抖计时器 + _debounceTimer = new DispatcherTimer + { + Interval = TimeSpan.FromMilliseconds(DebounceDelayMs) + }; + + excludeCheckBox.Click += delegate + { + _ = ApplyFilterToExistingNodesAsync(list, filterTextBox.Text, excludeCheckBox.IsChecked, deepCheckBox.IsChecked); + }; + deepCheckBox.Click += delegate + { + _ = ApplyFilterToExistingNodesAsync(list, filterTextBox.Text, excludeCheckBox.IsChecked, deepCheckBox.IsChecked); + }; + filterTextBox.TextChanged += delegate + { + _debounceTimer.Stop(); + + // 设置计时器回调 + _debounceTimer.Tick -= OnDebounceTimerTick; + _debounceTimer.Tick += OnDebounceTimerTick; + + _debounceTimer.Start(); + void OnDebounceTimerTick(object? sender, EventArgs e) + { + _debounceTimer.Stop(); + _debounceTimer.Tick -= OnDebounceTimerTick; + _ = ApplyFilterToExistingNodesAsync(list, filterTextBox.Text, excludeCheckBox.IsChecked, deepCheckBox.IsChecked); + } + }; + + stackPanel.Children.Add(excludeCheckBox); + stackPanel.Children.Add(deepCheckBox); stackPanel.Children.Add(filterTextBox); - AddNodesToPanel(stackPanel, list, 0, filterTextBox.Text, deepCheckBox.IsChecked); + + // 异步构建UI树 + await BuildCompleteUITreeAsync(stackPanel, list, 0); + + filterTextBox.Focus(); var scrollViewer = new ScrollViewer { Content = stackPanel, VerticalScrollBarVisibility = ScrollBarVisibility.Auto, - //Height = 435 // 固定高度 }; return scrollViewer; } /// - /// 应用筛选条件并更新面板显示的文件树节点 + /// 异步构建完整的UI树 /// - /// 要更新的父面板 - /// 要处理的文件树节点集合 - /// 用户输入的筛选关键词 - /// 是否排除已选择的文件夹 - /// 是否启用深度搜索 - private void ApplyFilter(StackPanel parentPanel, IEnumerable> nodes, string filter, bool? excludeSelectedFolder = false, bool? isDeepSearch = false) + /// 构建内容的父容器 + /// 要构建的节点集合 + /// 构建的深度 + private async Task BuildCompleteUITreeAsync(StackPanel parentPanel, IEnumerable>? nodes, int depth) { - if (parentPanel.Children.Count > 0) + if (nodes == null) + return; + + var nodeList = nodes.ToList(); + + for (int i = 0; i < nodeList.Count; i += 1) { - List removeElements = new List(); - foreach (UIElement parentPanelChild in parentPanel.Children) + var batch = nodeList.Skip(i).Take(1); + + // 在UI线程中创建UI元素并添加到面板 + await Application.Current.Dispatcher.InvokeAsync(async () => { - if (parentPanelChild is FrameworkElement frameworkElement && frameworkElement.Name.StartsWith("dynamic_")) + foreach (var node in batch) { - removeElements.Add(frameworkElement); + var element = CreateUIElementForNode(node, depth); + parentPanel.Children.Add(element); + + // 如果是目录且有子节点,递归构建子节点 + if (node.IsDirectory && node.Children?.Any() == true && element is Expander expander) + { + if (expander.Content is StackPanel childPanel) + { + await BuildCompleteUITreeAsync(childPanel, node.Children, depth + 1); + } + } } - } + }, DispatcherPriority.Background); - removeElements.ForEach(parentPanel.Children.Remove); + // 让出控制权,避免长时间阻塞UI线程 + await Task.Delay(1); } - - if (excludeSelectedFolder ?? false) + } + + /// + /// 为单个节点创建UI元素 + /// + /// 文件树节点 + /// 节点深度 + /// 创建的UI元素 + private FrameworkElement CreateUIElementForNode(FileTreeNode node, int depth) + { + var checkBox = new CheckBox { - List skipFolderNames = SelectedScriptGroup?.Projects.ToList().Select(item => item.FolderName).Distinct().ToList() ?? []; - //复制Nodes - string jsonString = JsonSerializer.Serialize(nodes); - var copiedNodes = JsonSerializer.Deserialize>>(jsonString); - if (copiedNodes != null) + Content = node.FileName, + Tag = node.FilePath, + Margin = new Thickness(depth * 30, 0, 0, 0), + Name = "dynamic_" + Guid.NewGuid().ToString().Replace("-", "_") + }; + + // 存储路径与UI元素的映射 + if (!string.IsNullOrEmpty(node.FilePath)) + _nodeUIElements[node.FilePath] = checkBox; + + if (node.IsDirectory) + { + // 如果父节点没有任何子内容,则不可勾选 + if (node.Children == null || node.Children.Count == 0) + checkBox.IsEnabled = false; + + var childPanel = new StackPanel(); + checkBox.IsThreeState = true; + var expander = new Expander { - //路径过滤 - copiedNodes = FileTreeNodeHelper.FilterTree(copiedNodes, skipFolderNames); - copiedNodes = FileTreeNodeHelper.FilterEmptyNodes(copiedNodes); - AddNodesToPanel(parentPanel, copiedNodes, 0, filter, isDeepSearch); + Header = checkBox, + Content = childPanel, + IsExpanded = false, + Name = "dynamic_" + Guid.NewGuid().ToString().Replace("-", "_"), + Visibility = Visibility.Visible + }; + + // 存储路径与UI元素的映射 + if (!string.IsNullOrEmpty(node.FilePath)) + { + _nodeUIElements[node.FilePath + "_expander"] = expander; } + + // 修改事件处理:用户点击时只在全选和全不选之间切换 + checkBox.Click += (s, e) => HandleDirectoryCheckBoxClick(checkBox, childPanel); + + return expander; } else { - AddNodesToPanel(parentPanel, nodes, 0, filter, isDeepSearch); + // 为文件复选框添加状态改变事件,用于更新父级状态 + checkBox.Checked += (s, e) => UpdateParentCheckBoxState(checkBox); + checkBox.Unchecked += (s, e) => UpdateParentCheckBoxState(checkBox); + + return checkBox; } } /// - /// 递归地将文件树节点添加到面板中,支持筛选和深度控制 + /// 异步应用筛选到已存在的节点 + /// 要筛选的节点集合 + /// 用户输入的筛选关键词 + /// 排除选择的目录 + /// 深度搜索功能 /// - /// 要添加节点的父面板 - /// 要处理的文件树节点集合 - /// 当前节点在树中的深度级别 - /// 用户输入的筛选关键词,为空时显示所有节点 - /// 是否启用深度搜索 - /// 当前节点的父级是否已经匹配筛选条件 - /// 返回是否在当前层级找到了直接匹配的节点以用于递归 - private bool AddNodesToPanel(StackPanel parentPanel, IEnumerable> nodes, int depth, string filter, bool? isDeepSearch = false, bool parentMatched = false) + private async Task ApplyFilterToExistingNodesAsync(IEnumerable> nodes, string filter, bool? excludeSelectedFolder = false, bool? isDeepSearch = false) { - bool containsDirectMatch = false; + var filteredResult = await Task.Run(() => + { + IEnumerable> filteredNodes = nodes; + + // 如果启用排除已选择过的目录,先过滤掉这些目录 + if (excludeSelectedFolder ?? false) + { + List skipFolderNames = SelectedScriptGroup?.Projects.ToList().Select(item => item.FolderName).Distinct().ToList() ?? []; + string jsonString = JsonSerializer.Serialize(nodes); + var copiedNodes = JsonSerializer.Deserialize>>(jsonString); + if (copiedNodes != null) + { + copiedNodes = FileTreeNodeHelper.FilterTree(copiedNodes, skipFolderNames); + copiedNodes = FileTreeNodeHelper.FilterEmptyNodes(copiedNodes); + filteredNodes = copiedNodes; + } + } + + return filteredNodes; + }); + + // 在UI线程中更新可见性 + await Application.Current.Dispatcher.InvokeAsync(() => + { + // 重置所有节点的可见性 + foreach (var element in _nodeUIElements.Values) + element.Visibility = Visibility.Collapsed; + + UpdateNodesVisibility(filteredResult, filter, isDeepSearch); + }, DispatcherPriority.Background); + } + + /// + /// 更新节点的可见性和展开状态 + /// + /// 要处理的文件树节点集合 + /// 用户输入的筛选关键词 + /// 是否启用深度搜索 + /// 当前节点在树中的深度级别 + /// 当前节点的父级是否已经匹配筛选条件 + /// 是否返回匹配状态(用于子节点处理) + /// returnMatchStatus则返回是否包含匹配的节点 + private bool UpdateNodesVisibility(IEnumerable> nodes, string filter, bool? isDeepSearch, int depth = 0, bool parentMatched = false, bool returnMatchStatus = false) + { + bool containsMatch = false; foreach (var node in nodes) { - // 过滤不符合条件的节点 - if (!ShouldShowNode(node, filter, isDeepSearch, depth, parentMatched)) + if (string.IsNullOrEmpty(node.FilePath)) continue; - var checkBox = new CheckBox + bool nodeMatches = !string.IsNullOrEmpty(filter) && IsNodeMatched(node, filter); + bool shouldShow = ShouldShowNode(node, filter, isDeepSearch, depth, parentMatched); + + // 更新节点可见性 + if (_nodeUIElements.TryGetValue(node.FilePath, out var element)) { - Content = node.FileName, - Tag = node.FilePath, - Margin = new Thickness(depth * 30, 0, 0, 0), // 根据深度计算Margin - Name = "dynamic_" + Guid.NewGuid().ToString().Replace("-", "_") - }; - - if (node.IsDirectory) - { - var childPanel = new StackPanel(); - - // 获取父文件夹名称,用于特殊深度控制规则(因“地方特产”目录中的详细项目的深度与其他目录不同) - string? parentFolderName = GetParentFolderName(node); - - // 获取当前节点是否匹配 - bool nodeMatches = !string.IsNullOrEmpty(filter) && IsNodeMatched(node, filter); - - // 判断是否应该处理子节点 - // 1. 无筛选条件,总是处理 - // 2. 有筛选条件,只有深度允许下才处理 - bool shouldAddChildren = string.IsNullOrEmpty(filter) || depth < GetMaxDepth(isDeepSearch, parentFolderName, nodeMatches, parentMatched); - - // 递归处理子节点 - // 1. 只有在应该添加子节点时才进行递归调用 - // 2. 传入更新的匹配状态:当前节点匹配或当前节点的父节点匹配 - // 3. 返回值表示该节点的子树中是否包含匹配的节点 - bool childContainsMatch = shouldAddChildren && - AddNodesToPanel(childPanel, node.Children, depth + 1, filter, isDeepSearch, nodeMatches || parentMatched); - - // 如果子树中包含匹配,当前层级也标记为包含匹配 - if (childContainsMatch) - containsDirectMatch = true; - - // 如果当前节点匹配,也标记为包含匹配 - if (nodeMatches) - containsDirectMatch = true; - - var expander = new Expander - { - Header = checkBox, - Content = childPanel, - IsExpanded = ShouldExpandNode(filter, nodeMatches, parentMatched, childContainsMatch, depth, isDeepSearch, parentFolderName), - Name = "dynamic_" + Guid.NewGuid().ToString().Replace("-", "_") - }; - - checkBox.Checked += (s, e) => SetChildCheckBoxesState(childPanel, true); - checkBox.Unchecked += (s, e) => SetChildCheckBoxesState(childPanel, false); - - parentPanel.Children.Add(expander); + element.Visibility = shouldShow ? Visibility.Visible : Visibility.Collapsed; + if (shouldShow && nodeMatches && returnMatchStatus) + containsMatch = true; } - else - { - parentPanel.Children.Add(checkBox); - // 如果是文件节点且匹配,标记为包含匹配 - if (!string.IsNullOrEmpty(filter) && IsNodeMatched(node, filter)) - containsDirectMatch = true; + // 如果是目录节点,递归处理子节点并更新展开状态 + if (node.IsDirectory && _nodeUIElements.TryGetValue(node.FilePath + "_expander", out var expanderElement) && expanderElement is Expander expander) + { + if (shouldShow) + { + // 递归处理子节点,传入returnMatchStatus = true来获取子节点匹配状态 + bool childContainsMatch = UpdateNodesVisibility(node.Children, filter, isDeepSearch, depth + 1, nodeMatches || parentMatched, true); + + // 如果子节点包含匹配且需要返回匹配状态,当前层级也标记为包含匹配 + if (childContainsMatch && returnMatchStatus) + containsMatch = true; + + expander.IsExpanded = ShouldExpandNode(filter, nodeMatches, parentMatched, childContainsMatch, depth, isDeepSearch, GetParentFolderName(node)); + expander.Visibility = Visibility.Visible; + } + else + { + expander.Visibility = Visibility.Collapsed; + } } } - return containsDirectMatch; + return containsMatch; } /// @@ -1082,8 +1203,7 @@ public partial class ScriptControlViewModel : ViewModel { foreach (var child in node.Children) { - // 递归时,传递当前节点的匹配状态 - // 每个子节点深度相同,所以如果递归过程中任意子节点应该显示,则当前节点也应该显示 + // 递归时,传递当前节点的匹配状态,任意当前深度的节点应该显示,则当前节点也应该显示 if (ShouldShowNode(child, filter, isDeepSearch, currentDepth + 1, currentNodeMatches)) return true; } @@ -1210,7 +1330,154 @@ public partial class ScriptControlViewModel : ViewModel return defaultDepth; } - private void SetChildCheckBoxesState(StackPanel childStackPanel, bool state) + /// + /// 处理目录复选框点击事件 + /// + /// 被点击的目录复选框 + /// 子面板 + private void HandleDirectoryCheckBoxClick(CheckBox checkBox, StackPanel childPanel) + { + var childCheckBoxes = GetAllChildCheckBoxes(childPanel); + + // 判断目标状态:如果所有子项都已选中,则全不选;否则全选 + bool allChildrenChecked = childCheckBoxes.Count > 0 && childCheckBoxes.All(cb => cb.IsChecked == true); + bool targetState = !allChildrenChecked; + + checkBox.IsChecked = targetState; + SetChildCheckBoxesState(childPanel, targetState); + UpdateParentCheckBoxState(checkBox); + } + + /// + /// 递归获取面板中所有的子复选框 + /// + /// 获取的面板 + /// 所有子复选框列表 + private List GetAllChildCheckBoxes(StackPanel panel) + { + var checkBoxes = new List(); + + foreach (var child in panel.Children) + { + if (child is CheckBox checkBox) + { + checkBoxes.Add(checkBox); + } + else if (child is Expander expander) + { + if (expander.Header is CheckBox headerCheckBox) + { + checkBoxes.Add(headerCheckBox); + } + + if (expander.Content is StackPanel nestedPanel) + { + checkBoxes.AddRange(GetAllChildCheckBoxes(nestedPanel)); + } + } + } + + return checkBoxes; + } + + /// + /// 更新父级复选框的三态状态 + /// + /// 状态发生改变的复选框 + private void UpdateParentCheckBoxState(CheckBox changedCheckBox) + { + // 查找父级复选框 + var parentCheckBox = FindParentCheckBox(changedCheckBox); + if (parentCheckBox == null) + return; + + // 获取同级所有复选框 + var siblingCheckBoxes = GetSiblingCheckBoxes(changedCheckBox); + + // 计算状态 + int checkedCount = siblingCheckBoxes.Count(cb => cb.IsChecked == true); + int uncheckedCount = siblingCheckBoxes.Count(cb => cb.IsChecked == false); + int indeterminateCount = siblingCheckBoxes.Count(cb => cb.IsChecked == null); + + // 设置父级复选框状态 + if (checkedCount == siblingCheckBoxes.Count) + parentCheckBox.IsChecked = true; + else if (uncheckedCount == siblingCheckBoxes.Count) + parentCheckBox.IsChecked = false; + else + parentCheckBox.IsChecked = null; + + // 递归更新上级父级 + UpdateParentCheckBoxState(parentCheckBox); + } + + /// + /// 查找指定复选框的父级复选框 + /// + /// 当前复选框 + /// 父级复选框,如果没有则返回null + private CheckBox? FindParentCheckBox(CheckBox checkBox) + { + var filePath = checkBox.Tag as string; + if (string.IsNullOrEmpty(filePath)) + return null; + + // 获取父目录路径 + var parentPath = Path.GetDirectoryName(filePath); + if (string.IsNullOrEmpty(parentPath)) + return null; + + // 查找父级复选框 + if (_nodeUIElements.TryGetValue(parentPath, out var parentElement) && parentElement is CheckBox parentCheckBox) + { + return parentCheckBox; + } + + return null; + } + + /// + /// 获取同级的所有复选框 + /// + /// 当前复选框 + /// 同级复选框列表 + private static List GetSiblingCheckBoxes(CheckBox checkBox) + { + // 先尝试获取逻辑父级 + var parent = LogicalTreeHelper.GetParent(checkBox) as FrameworkElement; + + // 如果逻辑父级不存在,尝试可视化父级 + parent ??= VisualTreeHelper.GetParent(checkBox) as FrameworkElement; + + // 如果当前元素是Expander的Header,获取Expander的父级 + if (parent is Expander expander) + { + parent = LogicalTreeHelper.GetParent(expander) as FrameworkElement ?? + VisualTreeHelper.GetParent(expander) as FrameworkElement; + } + + // 遍历同级元素 + var siblings = new List(); + if (parent is StackPanel stackPanel) + { + foreach (var child in stackPanel.Children) + { + if (child is CheckBox siblingCheckBox) + siblings.Add(siblingCheckBox); + else if (child is Expander childExpander && childExpander.Header is CheckBox expanderCheckBox) + siblings.Add(expanderCheckBox); + } + } + + return siblings; + } + + /// + /// 递归设置子复选框状态 + /// + /// 子面板 + /// 目标状态 + private static void SetChildCheckBoxesState(StackPanel childStackPanel, bool state) { foreach (var child in childStackPanel.Children) { @@ -1458,6 +1725,45 @@ public partial class ScriptControlViewModel : ViewModel ); } + [RelayCommand] + public void OnOpenScriptFolder(ScriptGroupProject? item) + { + if (item == null) + { + return; + } + + try + { + string? path = null; + switch (item.Type) + { + case "Javascript": + path = Path.Combine(Global.ScriptPath(), item.FolderName); + break; + case "KeyMouse": + path = Global.Absolute(@"User\KeyMouseScript"); + break; + case "Pathing": + path = Path.Combine(MapPathingViewModel.PathJsonPath, item.FolderName); + break; + } + + if (path != null && Directory.Exists(path)) + { + Process.Start("explorer.exe", path); + } + else + { + _snackbarService.Show("打开失败", "目录不存在", ControlAppearance.Caution, null, TimeSpan.FromSeconds(2)); + } + } + catch (Exception e) + { + _logger.LogDebug(e, "打开脚本目录失败"); + _snackbarService.Show("打开失败", e.Message, ControlAppearance.Danger, null, TimeSpan.FromSeconds(3)); + } + } private void ScriptGroupsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { if (e.NewItems != null) diff --git a/Test/BetterGenshinImpact.Test/Cv/ThresholdWindow.cs b/Test/BetterGenshinImpact.Test/Cv/ThresholdWindow.cs new file mode 100644 index 00000000..eb37d2eb --- /dev/null +++ b/Test/BetterGenshinImpact.Test/Cv/ThresholdWindow.cs @@ -0,0 +1,81 @@ +using OpenCvSharp; + +namespace BetterGenshinImpact.Test.Cv; + +public class ThresholdWindow +{ + private Mat? _originalImage; + private Mat? _grayImage; + private int _currentThreshold = 160; + + + public static void Test() + { + var window = new ThresholdWindow(); + window.ShowThresholdAdjuster(@"E:\HuiTask\更好的原神\自动拾取\pick_ocr_ori_20250915011455192.png"); + } + + /// + /// 对指定图片进行二值化阈值拉条调整 + /// + /// 图片路径 + public void ShowThresholdAdjuster(string imagePath) + { + // 加载原始图像 + _originalImage = Cv2.ImRead(imagePath); + if (_originalImage.Empty()) + { + throw new ArgumentException("无法加载图像文件"); + } + + // 转换为灰度图像 + _grayImage = new Mat(); + Cv2.CvtColor(_originalImage, _grayImage, ColorConversionCodes.BGR2GRAY); + + // 创建窗口 + const string windowName = "Threshold Adjuster"; + const string trackbarName = "Threshold"; + + Cv2.NamedWindow(windowName, WindowFlags.AutoSize); + + // 创建拉条,范围0-255 + Cv2.CreateTrackbar(trackbarName, windowName, ref _currentThreshold, 255, OnThresholdChanged); + + // 初始显示 + UpdateThresholdImage(windowName); + + // 等待用户按键 + Console.WriteLine("按任意键关闭窗口..."); + Cv2.WaitKey(0); + + // 清理资源 + Cv2.DestroyAllWindows(); + _originalImage?.Dispose(); + _grayImage?.Dispose(); + } + + /// + /// 阈值变化回调函数 + /// + /// 阈值 + /// 用户数据指针(未使用) + private void OnThresholdChanged(int value, IntPtr userdata) + { + _currentThreshold = value; + UpdateThresholdImage("Threshold Adjuster"); + } + + /// + /// 更新二值化图像显示 + /// + /// 窗口名称 + private void UpdateThresholdImage(string windowName) + { + if (_grayImage == null) return; + + using var thresholdImage = new Mat(); + Cv2.Threshold(_grayImage, thresholdImage, _currentThreshold, 255, ThresholdTypes.Binary); + + Cv2.ImShow(windowName, thresholdImage); + } +} \ No newline at end of file diff --git a/Test/BetterGenshinImpact.Test/Dataset/AvatarClassifyGen.cs b/Test/BetterGenshinImpact.Test/Dataset/AvatarClassifyGen.cs index 57dbf128..4979419a 100644 --- a/Test/BetterGenshinImpact.Test/Dataset/AvatarClassifyGen.cs +++ b/Test/BetterGenshinImpact.Test/Dataset/AvatarClassifyGen.cs @@ -7,17 +7,17 @@ namespace BetterGenshinImpact.Test.Dataset; public class AvatarClassifyGen { // 基础图像文件夹 - private const string BaseDir = @"E:\HuiTask\更好的原神\数据源\Snap.Static\AvatarIcon"; + private const string BaseDir = @"E:\HuiTask\更好的原神\侧面头像\源数据\AvatarIcon"; // 产出文件夹 - private const string OutputDir = @"E:\HuiAi\YOLOv8\3.avatar-side"; + private const string OutputDir = @"E:\HuiTask\更好的原神\侧面头像\dateset"; // 背景图像文件夹 - private static readonly string BackgroundDir = @"E:\HuiTask\更好的原神\数据源\background"; + private static readonly string BackgroundDir = @"E:\HuiTask\更好的原神\侧面头像\background"; private static readonly Random Rd = new Random(); - public static readonly List ImgNames = ["UI_AvatarIcon_Side_BennettCostumeSummer.png","UI_AvatarIcon_Side_YelanCostumeSummer.png", "UI_AvatarIcon_Side_Ineffa.png"]; + public static readonly List ImgNames = ["UI_AvatarIcon_Side_Aino.png","UI_AvatarIcon_Side_Flins.png", "UI_AvatarIcon_Side_Nefer.png", "UI_AvatarIcon_Side_Nefer.png", "UI_AvatarIcon_Side_Lauma.png"]; public static void GenAll() { diff --git a/Test/BetterGenshinImpact.Test/Dataset/AvatarClassifyTransparentGen.cs b/Test/BetterGenshinImpact.Test/Dataset/AvatarClassifyTransparentGen.cs index b1aca700..e011b517 100644 --- a/Test/BetterGenshinImpact.Test/Dataset/AvatarClassifyTransparentGen.cs +++ b/Test/BetterGenshinImpact.Test/Dataset/AvatarClassifyTransparentGen.cs @@ -7,13 +7,13 @@ namespace BetterGenshinImpact.Test.Dataset; public class AvatarClassifyTransparentGen { // 基础图像文件夹 - private const string BaseDir = @"E:\HuiTask\更好的原神\数据源\Snap.Static\AvatarIcon"; + private const string BaseDir = @"E:\HuiTask\更好的原神\侧面头像\源数据\AvatarIcon"; // 产出文件夹 - private const string OutputDir = @"E:\HuiAi\YOLOv8\3.avatar-side"; + private const string OutputDir = @"E:\HuiTask\更好的原神\侧面头像\dateset"; // 背景图像文件夹 - private static readonly string BackgroundDir = @"E:\HuiTask\更好的原神\数据源\background"; + private static readonly string BackgroundDir = @"E:\HuiTask\更好的原神\侧面头像\background"; private static readonly Random Rd = new Random(); diff --git a/Test/BetterGenshinImpact.Test/Simple/AllMap/LargeSIFTExtractor.cs b/Test/BetterGenshinImpact.Test/Simple/AllMap/LargeSIFTExtractor.cs index e704ec50..45ff0025 100644 --- a/Test/BetterGenshinImpact.Test/Simple/AllMap/LargeSIFTExtractor.cs +++ b/Test/BetterGenshinImpact.Test/Simple/AllMap/LargeSIFTExtractor.cs @@ -22,7 +22,7 @@ public class LargeSiftExtractor public static void Gen1024() { - var rootPath = @"E:\HuiTask\更好的原神\地图匹配\拼图结果\5.8"; + var rootPath = @"E:\HuiTask\更好的原神\地图匹配\拼图结果\6.0"; var mainMap2048BlockMat = new Mat($@"{rootPath}\map_2048.png", ImreadModes.Color); // 缩小 2048/1024 = 2 var targetFilePath = $@"{rootPath}\1024_map.png"; @@ -38,16 +38,21 @@ public class LargeSiftExtractor { Environment.SetEnvironmentVariable("OPENCV_IO_MAX_IMAGE_PIXELS", Math.Pow(2, 40).ToString("F0")); - var rootPath = @"E:\HuiTask\更好的原神\地图匹配\拼图结果\5.8"; - var mainMap2048BlockMat = new Mat($@"{rootPath}\map_2048.png", ImreadModes.Color); + var rootPath = @"E:\HuiTask\更好的原神\地图匹配\拼图结果\6.0"; + // 缩小 2048/256 = 8 var targetFilePath = $@"{rootPath}\Teyvat_0_256.png"; - // opencv 缩小 - var mainMap256BlockMat = - mainMap2048BlockMat.Resize(new Size(mainMap2048BlockMat.Width / 8, mainMap2048BlockMat.Height / 8)); - // 转化为灰度图 - mainMap256BlockMat = mainMap256BlockMat.CvtColor(ColorConversionCodes.BGR2GRAY); - mainMap256BlockMat.SaveImage(targetFilePath); + if (!File.Exists(targetFilePath)) + { + var mainMap2048BlockMat = new Mat($@"{rootPath}\map_2048.png", ImreadModes.Color); + // opencv 缩小 + var mainMap256BlockMat = + mainMap2048BlockMat.Resize(new Size(mainMap2048BlockMat.Width / 8, mainMap2048BlockMat.Height / 8)); + // 转化为灰度图 + mainMap256BlockMat = mainMap256BlockMat.CvtColor(ColorConversionCodes.BGR2GRAY); + mainMap256BlockMat.SaveImage(targetFilePath); + } + FeatureMatcher featureMatcher = new(new Mat(targetFilePath, ImreadModes.Grayscale), new FeatureStorage("Teyvat_0_256", rootPath)); @@ -58,8 +63,8 @@ public class LargeSiftExtractor { Environment.SetEnvironmentVariable("OPENCV_IO_MAX_IMAGE_PIXELS", Math.Pow(2, 40).ToString("F0")); var extractor = new LargeSiftExtractor(); - extractor.ExtractAndSaveSift(@"E:\HuiTask\更好的原神\地图匹配\拼图结果\5.8\map_2048.png", - @"E:\HuiTask\更好的原神\地图匹配\拼图结果\5.8\"); + extractor.ExtractAndSaveSift(@"E:\HuiTask\更好的原神\地图匹配\拼图结果\6.0\map_2048.png", + @"E:\HuiTask\更好的原神\地图匹配\拼图结果\6.0\"); } public void ExtractAndSaveSift(string imagePath, string outputPath) diff --git a/Test/BetterGenshinImpact.Test/Simple/AllMap/MapPuzzle.cs b/Test/BetterGenshinImpact.Test/Simple/AllMap/MapPuzzle.cs index 33975a9a..2cf1dd67 100644 --- a/Test/BetterGenshinImpact.Test/Simple/AllMap/MapPuzzle.cs +++ b/Test/BetterGenshinImpact.Test/Simple/AllMap/MapPuzzle.cs @@ -27,18 +27,21 @@ public class MapPuzzle public static void PutAll() { + var folder = @"E:\HuiTask\更好的原神\地图匹配\拼图结果\6.0"; + Directory.CreateDirectory(folder); + // 保存2048大图 var img2048 = Put(2048); - Cv2.ImWrite(@"E:\HuiTask\更好的原神\地图匹配\拼图结果\5.8\map_2048.png", img2048); + Cv2.ImWrite(@$"{folder}\map_2048.png", img2048); // 保存1024 var img1024 = Put(1024); - Cv2.ImWrite(@"E:\HuiTask\更好的原神\地图匹配\拼图结果\5.8\map_1024.png", img1024); + Cv2.ImWrite(@$"{folder}\map_1024.png", img1024); // 保存256 var grayImage = new Mat(); Cv2.CvtColor(img2048.Resize(new Size(img2048.Width / 8, img2048.Height / 8), 0, 0, InterpolationFlags.Cubic), grayImage, ColorConversionCodes.BGR2GRAY); - Cv2.ImWrite(@"E:\HuiTask\更好的原神\地图匹配\拼图结果\5.8\map_256.png", grayImage); + Cv2.ImWrite(@$"{folder}\Teyvat_0_256.png", grayImage); } diff --git a/Test/BetterGenshinImpact.UnitTest/CoreTests/RecognitionTests/OCRTests/PaddleFixture.cs b/Test/BetterGenshinImpact.UnitTest/CoreTests/RecognitionTests/OCRTests/PaddleFixture.cs index 28f488ee..fd661a16 100644 --- a/Test/BetterGenshinImpact.UnitTest/CoreTests/RecognitionTests/OCRTests/PaddleFixture.cs +++ b/Test/BetterGenshinImpact.UnitTest/CoreTests/RecognitionTests/OCRTests/PaddleFixture.cs @@ -9,16 +9,26 @@ namespace BetterGenshinImpact.UnitTest.CoreTests.RecognitionTests.OCRTests { private readonly ConcurrentDictionary _paddleOcrServices = new(); - public PaddleOcrService Get(string cultureInfoName = "zh-Hans") + public PaddleOcrService Get(string cultureInfoName = "zh-Hans", string version = "V5") { return _paddleOcrServices.GetOrAdd(cultureInfoName, name => { lock (_paddleOcrServices) { - return new PaddleOcrService( - new BgiOnnxFactory(new FakeLogger()), - PaddleOcrService.PaddleOcrModelType.FromCultureInfo(new CultureInfo(name)) ?? - PaddleOcrService.PaddleOcrModelType.V5); + if (version == "V5") + { + return new PaddleOcrService(new BgiOnnxFactory(new FakeLogger()), + PaddleOcrService.PaddleOcrModelType.FromCultureInfo(new CultureInfo(name)) ?? PaddleOcrService.PaddleOcrModelType.V5); + } + else if (version == "V4") + { + return new PaddleOcrService(new BgiOnnxFactory(new FakeLogger()), + PaddleOcrService.PaddleOcrModelType.FromCultureInfoV4(new CultureInfo(name)) ?? PaddleOcrService.PaddleOcrModelType.V4); + } + else + { + throw new NotSupportedException(); + } } }); } diff --git a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoDomainTests/ResinStatusTests.cs b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoDomainTests/ResinStatusTests.cs new file mode 100644 index 00000000..06d49f58 --- /dev/null +++ b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoDomainTests/ResinStatusTests.cs @@ -0,0 +1,45 @@ +using BetterGenshinImpact.GameTask.AutoDomain.Model; +using BetterGenshinImpact.GameTask.Model.Area; +using BetterGenshinImpact.GameTask.Model.Area.Converter; +using BetterGenshinImpact.UnitTest.CoreTests.RecognitionTests.OCRTests; +using OpenCvSharp; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoDomainTests +{ + [Collection("Init Collection")] + public class ResinStatusTests + { + private readonly PaddleFixture paddle; + + public ResinStatusTests(PaddleFixture paddle) + { + this.paddle = paddle; + } + + [Theory] + [InlineData(@"AutoDomain\SelectRevitalization.png", 21, 0, 2, 1)] + [InlineData(@"AutoDomain\SelectRevitalizationOcrV4.png", 11, 0, 1, 149, "V4")] + /// + /// 测试识别四种树脂数量,数量应正确 + /// + public void RecogniseFromRegion_ResinStatusShouldBeRight(string screenshot1080p, int originalResinCount, int fragileResinCount, int condensedResinCount, int transientResinCount, string ocrVersion = "V5") + { + // + Mat mat = new Mat(@$"..\..\..\Assets\{screenshot1080p}"); + var imageRegion = new ImageRegion(mat, 0, 0, converter: new ScaleConverter(1d)); + FakeSystemInfo systemInfo = new FakeSystemInfo(new Vanara.PInvoke.RECT(0, 0, mat.Width, mat.Height), 1); + + // + var result = ResinStatus.RecogniseFromRegion(imageRegion, systemInfo, this.paddle.Get(version: ocrVersion)); // todo:System.Exception : 未找到原粹树脂图标 + + // + Assert.Equal(originalResinCount, result.OriginalResinCount); + Assert.Equal(condensedResinCount, result.CondensedResinCount); + } + } +} diff --git a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFishingTests/BehavioursTests.ChooseBait.cs b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFishingTests/BehavioursTests.ChooseBait.cs index d5cfa27d..42c99678 100644 --- a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFishingTests/BehavioursTests.ChooseBait.cs +++ b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFishingTests/BehavioursTests.ChooseBait.cs @@ -3,6 +3,7 @@ using BetterGenshinImpact.GameTask.AutoFishing; using BetterGenshinImpact.GameTask.AutoFishing.Model; using BetterGenshinImpact.GameTask.Model.Area; using BetterGenshinImpact.GameTask.Model.Area.Converter; +using BetterGenshinImpact.Helpers.Extensions; using Microsoft.Extensions.Time.Testing; using OpenCvSharp; using System; @@ -15,10 +16,36 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests { public partial class BehavioursTests { + [Fact] + /// + /// 测试识别数量不足的鱼饵,由于图标变灰,识别应失败 + /// + public void FindBaitTest_RecognitionShouldFail() + { + // + Mat mat = new Mat(@$"..\..\..\Assets\AutoFishing\202509141339218213_ChooseBait.png"); + var imageRegion = new ImageRegion(mat, 0, 0, new DesktopRegion(new FakeMouseSimulator()), converter: new ScaleConverter(1d)); + + FakeSystemInfo systemInfo = new FakeSystemInfo(new Vanara.PInvoke.RECT(0, 0, mat.Width, mat.Height), 1); + var blackboard = new Blackboard(); + + // + ChooseBait sut = new ChooseBait("-", blackboard, new FakeLogger(), false, systemInfo, new FakeInputSimulator(), this.session, this.prototypes); + var result = sut.FindBait(imageRegion).OrderBy(r => r.Item1.X).ToArray(); + + // + Assert.Equal(3, result.Length); + Assert.Equal(BaitType.FruitPasteBait.GetDescription(), result[0].Item2); + Assert.Equal(BaitType.BerryBait.GetDescription(), result[1].Item2); + Assert.Null(result[2].Item2); + } + [Theory] [InlineData(@"20250225101300361_ChooseBait_Succeeded.png", new string[] { "medaka", "butterflyfish", "butterflyfish", "pufferfish" })] - [InlineData(@"20250226161354285_ChooseBait_Succeeded.png", new string[] { "medaka", "medaka" })] // todo 更新用例 + [InlineData(@"20250226161354285_ChooseBait_Succeeded.png", new string[] { "medaka" })] // 不稳定的测试用例,因未学习被照亮的场景 [InlineData(@"202503160917566615@900p.png", new string[] { "pufferfish" })] + [InlineData(@"202509141339218213_ChooseBait.png", new string[] { "axehead fish" })] + [InlineData(@"202509141339218213_ChooseBait.png", new string[] { "mauler shark", "crystal eye", "medaka", "medaka", "medaka" })] /// /// 测试各种选取鱼饵,结果为成功 /// @@ -50,6 +77,7 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests [Theory] [InlineData(@"20250226161354285_ChooseBait_Succeeded.png", new string[] { "koi" })] + [InlineData(@"202509141339218213_ChooseBait.png", new string[] { "mauler shark", "crystal eye" })] /// /// 测试各种选取鱼饵,结果为失败 /// diff --git a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFishingTests/BehavioursTests.ThrowRod.cs b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFishingTests/BehavioursTests.ThrowRod.cs index 5c0bc914..3c0dd8d8 100644 --- a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFishingTests/BehavioursTests.ThrowRod.cs +++ b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFishingTests/BehavioursTests.ThrowRod.cs @@ -14,7 +14,6 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests { [Theory] [InlineData(@"20250225101304534_ThrowRod_Succeeded.png", BaitType.FalseWormBait)] - [InlineData(@"20250226162217468_ThrowRod_Succeeded.png", BaitType.FruitPasteBait)] /// /// 测试各种抛竿,结果为成功 /// @@ -43,6 +42,7 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests [Theory] [InlineData(@"20250225101304534_ThrowRod_Succeeded.png", BaitType.RedrotBait)] [InlineData(@"20250225101304534_ThrowRod_Succeeded.png", BaitType.FakeFlyBait)] + [InlineData(@"20250226162217468_ThrowRod_Succeeded.png", BaitType.FruitPasteBait)] /// /// 测试各种抛竿,未满足HutaoFisher判定,结果为运行中 /// diff --git a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFishingTests/FakeSystemInfo.cs b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/FakeSystemInfo.cs similarity index 90% rename from Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFishingTests/FakeSystemInfo.cs rename to Test/BetterGenshinImpact.UnitTest/GameTaskTests/FakeSystemInfo.cs index 1a05badc..209c2e33 100644 --- a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFishingTests/FakeSystemInfo.cs +++ b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/FakeSystemInfo.cs @@ -1,4 +1,4 @@ -using BetterGenshinImpact.GameTask.Model; +using BetterGenshinImpact.GameTask.Model; using BetterGenshinImpact.GameTask.Model.Area; using OpenCvSharp; using System; @@ -9,7 +9,7 @@ using System.Text; using System.Threading.Tasks; using Vanara.PInvoke; -namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests +namespace BetterGenshinImpact.UnitTest.GameTaskTests { internal class FakeSystemInfo : ISystemInfo { @@ -29,7 +29,7 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests public RECT GameScreenSize { get; } - public double AssetScale { get; } + public double AssetScale { get; } = 1; public double ZoomOutMax1080PRatio { get; } diff --git a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/GetGridIconsTests/GridIconsAccuracyTestTaskTests.cs b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/GetGridIconsTests/GridIconsAccuracyTestTaskTests.cs index ece8e548..7297d158 100644 --- a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/GetGridIconsTests/GridIconsAccuracyTestTaskTests.cs +++ b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/GetGridIconsTests/GridIconsAccuracyTestTaskTests.cs @@ -27,8 +27,8 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.GetGridIconsTests yield return new object[] { @"GetGridIcons\FoodGrid.png", 8, true, new[] { ("苹果", 0), ("日落果", 0), ("星蕈", 0), ("泡泡桔", 0), ("烛伞蘑菇", 0), ("宝石闪闪", 4), ("咚咚", 4), ("枫达", 2), ("雾凇秋分", 4), ("蒙德土豆饼", 3), /*("爪爪土豆饼", 3), ("北地苹果焖肉", 3), ("四方和平", 3), ("盛世太平", 3), ("三彩团子", 3), ("夏祭游鱼", 3)*/} }; // todo 爪爪土豆饼被吃掉了没进训练集。。。 - yield return new object[] { @"GetGridIcons\FoodGrid_Attack.png", 8, false, new[] { ("果果软糖", 3), ("方块戏法", 3), ("「缥雨一滴」", 3), ("繁弦急管", 3), ("繁弦急管", 3), ("「簇火赞歌」", 3), ("轻策家常菜", 3), ("冒险家蛋堡", 3), ("冒险家蛋堡", 3), ("测绘员蛋堡", 3), ("串串三味", 3), ("连心面", 3), ("摩拉急速来", 3), ("双果清露", 3), ("双果清露", 3), ("双果清露", 3), ("四喜圆满", 3), ("祝圣交响乐", 3), ("满足沙拉", 2), ("满足沙拉", 2), ("至高的智慧(生活)", 2), ("摇·滚·鸡!", 2), ("岩港三鲜", 2), ("鲜鱼燉萝卜", 2), ("炸萝卜丸子", 2), ("炸萝卜丸子", 2), ("杏仁豆腐", 2), ("杏仁豆腐", 2), ("「美梦」", 2), ("凉拌薄荷", 2), ("炸肉排三明治", 2), ("唯一的真相", 2) } }; - yield return new object[] { @"GetGridIcons\MaterialGrid_TreesAndBaits.png", 8, false, new[] { ("松木", 1), ("却砂木", 1), ("竹节", 1), ("垂香木", 1), ("杉木", 1), ("梦见木", 1), ("枫木", 1), ("孔雀木", 1), ("御伽木", 1), ("辉木", 1), ("业果木", 1), ("证悟木", 1), ("枫木", 1), ("黑铜号角", 1), ("悬铃木", 1), ("白梣木", 1), ("香柏木", 1), ("白栗栎木", 1), ("灰灰楼林木", 1), ("燃爆木", 1), ("布匹", 0), ("红色染料", 0), ("黄色染料", 0), ("蓝色染料", 0), ("果酿饵", 2), ("赤糜饵", 2), ("蠕虫假饵", 2), ("飞蝇假饵", 2), ("甘露饵", 2), ("酸桔饵", 2), ("维护机关频闪诱饵", 2), ("澄晶果粒饵", 2) } }; + yield return new object[] { @"GetGridIcons\FoodGrid_Attack.png", 8, false, new[] { ("果果软糖", 3), ("方块戏法", 3), ("「缥雨一滴」", 3), ("繁弦急管", 3), ("繁弦急管", 3), ("「簇火赞歌」", 3), ("轻策家常菜", 3), ("冒险家蛋堡", 3), ("冒险家蛋堡", 3), ("测绘员蛋堡", 3), ("串串三味", 3), ("连心面", 3), ("摩拉急速来", 3), ("双果清露", 3), ("双果清露", 3), ("双果清露", 3), ("四喜圆满", 3), ("祝圣交响乐", 3), ("满足沙拉", 2), ("满足沙拉", 2), ("至高的智慧(生活)", 2), ("摇·滚·鸡!", 2), ("岩港三鲜", 2), ("鲜鱼炖萝卜", 2), ("炸萝卜丸子", 2), ("炸萝卜丸子", 2), ("杏仁豆腐", 2), ("杏仁豆腐", 2), ("「美梦」", 2), ("凉拌薄荷", 2), ("炸肉排三明治", 2), ("唯一的真相", 2) } }; + yield return new object[] { @"GetGridIcons\MaterialGrid_TreesAndBaits.png", 8, false, new[] { ("松木", 1), ("却砂木", 1), ("竹节", 1), ("垂香木", 1), ("杉木", 1), ("梦见木", 1), ("枫木", 1), ("孔雀木", 1), ("御伽木", 1), ("辉木", 1), ("业果木", 1), ("证悟木", 1), ("刺葵木", 1), ("柽木", 1), ("悬铃木", 1), ("白梣木", 1), ("香柏木", 1), ("白栗栎木", 1), ("灰灰楼林木", 1), ("燃爆木", 1), ("布匹", 0), ("红色染料", 0), ("黄色染料", 0), ("蓝色染料", 0), ("果酿饵", 2), ("赤糜饵", 2), ("蠕虫假饵", 2), ("飞蝇假饵", 2), ("甘露饵", 2), ("酸桔饵", 2), ("维护机关频闪诱饵", 2), ("澄晶果粒饵", 2) } }; // string.Join(", ",result.Select(s=>$"(\"{s.Item1}\", {s.Item2})")) }