diff --git a/BetterGenshinImpact/BetterGenshinImpact.csproj b/BetterGenshinImpact/BetterGenshinImpact.csproj index 9552147e..15a6eae5 100644 --- a/BetterGenshinImpact/BetterGenshinImpact.csproj +++ b/BetterGenshinImpact/BetterGenshinImpact.csproj @@ -2,7 +2,7 @@ BetterGI - 0.49.0 + 0.49.1-alpha.3 false WinExe net8.0-windows10.0.22621.0 @@ -43,7 +43,7 @@ - + @@ -177,12 +177,6 @@ Always - - PreserveNewest - - - PreserveNewest - Always @@ -192,6 +186,9 @@ Always + + Always + diff --git a/BetterGenshinImpact/Core/Config/CommonConfig.cs b/BetterGenshinImpact/Core/Config/CommonConfig.cs index 6e1f78d7..f29671b6 100644 --- a/BetterGenshinImpact/Core/Config/CommonConfig.cs +++ b/BetterGenshinImpact/Core/Config/CommonConfig.cs @@ -1,4 +1,5 @@ -using CommunityToolkit.Mvvm.ComponentModel; +using BetterGenshinImpact.Helpers; +using CommunityToolkit.Mvvm.ComponentModel; using System; using System.Collections.Generic; using Wpf.Ui.Controls; @@ -47,7 +48,7 @@ public partial class CommonConfig : ObservableObject /// 当前主题类型(新版主题) /// [ObservableProperty] - private ThemeType _currentThemeType = ThemeType.DarkMica; + private ThemeType _currentThemeType = OsVersionHelper.IsWindows11_22523_OrGreater? ThemeType.DarkMica : ThemeType.DarkNone; /// /// 主题(旧版主题,兼容性保留) diff --git a/BetterGenshinImpact/Core/Config/OtherConfig.cs b/BetterGenshinImpact/Core/Config/OtherConfig.cs index 7add1066..9ebc64d5 100644 --- a/BetterGenshinImpact/Core/Config/OtherConfig.cs +++ b/BetterGenshinImpact/Core/Config/OtherConfig.cs @@ -101,7 +101,7 @@ public partial class OtherConfig : ObservableObject /// PaddleOCR模型配置 /// [ObservableProperty] - private PaddleOcrModelConfig _paddleOcrModelConfig = PaddleOcrModelConfig.V4; + private PaddleOcrModelConfig _paddleOcrModelConfig = PaddleOcrModelConfig.V4Auto; } //public partial class OtherConfig : ObservableObject diff --git a/BetterGenshinImpact/Core/Recognition/OCR/OcrFactory.cs b/BetterGenshinImpact/Core/Recognition/OCR/OcrFactory.cs index f6af2f9d..6ff3cdaf 100644 --- a/BetterGenshinImpact/Core/Recognition/OCR/OcrFactory.cs +++ b/BetterGenshinImpact/Core/Recognition/OCR/OcrFactory.cs @@ -89,6 +89,10 @@ public class OcrFactory : IDisposable { return _config.PaddleOcrModelConfig switch { + PaddleOcrModelConfig.V4Auto => + new PaddleOcrService(App.ServiceProvider.GetRequiredService(), + PaddleOcrService.PaddleOcrModelType.FromCultureInfoV4(GetCultureInfo()) ?? + PaddleOcrService.PaddleOcrModelType.V4), PaddleOcrModelConfig.V5Auto => new PaddleOcrService(App.ServiceProvider.GetRequiredService(), PaddleOcrService.PaddleOcrModelType.FromCultureInfo(GetCultureInfo()) ?? diff --git a/BetterGenshinImpact/Core/Recognition/OCR/Paddle/PaddleOcrService.cs b/BetterGenshinImpact/Core/Recognition/OCR/Paddle/PaddleOcrService.cs index 00c22a4f..268767c8 100644 --- a/BetterGenshinImpact/Core/Recognition/OCR/Paddle/PaddleOcrService.cs +++ b/BetterGenshinImpact/Core/Recognition/OCR/Paddle/PaddleOcrService.cs @@ -201,6 +201,43 @@ public class PaddleOcrService : IOcrService, IDisposable return null; } + + /// + /// 中英文优先使用V4模型,其他语言使用V5模型 + /// + /// + /// + public static PaddleOcrModelType? FromCultureInfoV4(CultureInfo cultureInfo) + { + var v5 = FromCultureInfo(cultureInfo); + // 如果用的是v5, 那么优先用V4的细分模型 + if (v5 == V5) + { + List names = + [ + cultureInfo.EnglishName.ToLowerInvariant(), cultureInfo.Name.ToLowerInvariant(), + cultureInfo.ThreeLetterISOLanguageName.ToLowerInvariant(), + cultureInfo.TwoLetterISOLanguageName.ToLowerInvariant() + ]; + foreach (var name in names) + { + if (name.Equals("en")) + { + return V4En; + } + else if (name.Equals("zh-hant") || name.Equals("zh-tw") || name.Equals("zh-hk")) + { + return V5; + } + } + + return V4; + } + else + { + return v5; + } + } } public PaddleOcrService(BgiOnnxFactory bgiOnnxFactory, PaddleOcrModelType modelType) diff --git a/BetterGenshinImpact/Core/Recognition/PaddleOcrModelConfig.cs b/BetterGenshinImpact/Core/Recognition/PaddleOcrModelConfig.cs index 9770a073..acb1e7d0 100644 --- a/BetterGenshinImpact/Core/Recognition/PaddleOcrModelConfig.cs +++ b/BetterGenshinImpact/Core/Recognition/PaddleOcrModelConfig.cs @@ -2,11 +2,12 @@ public enum PaddleOcrModelConfig { - V5Auto, - V5, - V4, - V4En, - V5Latin, - V5Eslav, - V5Korean, + V4Auto = 2, + V5Auto = 0, + V4 = 7, + V4En = 3, + V5 = 1, + V5Latin = 4, + V5Eslav = 5, + V5Korean = 6, } \ No newline at end of file diff --git a/BetterGenshinImpact/Core/Script/Dependence/Dispatcher.cs b/BetterGenshinImpact/Core/Script/Dependence/Dispatcher.cs index a2dac83a..539d24cc 100644 --- a/BetterGenshinImpact/Core/Script/Dependence/Dispatcher.cs +++ b/BetterGenshinImpact/Core/Script/Dependence/Dispatcher.cs @@ -7,14 +7,14 @@ using BetterGenshinImpact.GameTask.AutoFishing; using BetterGenshinImpact.GameTask.AutoGeniusInvokation; using BetterGenshinImpact.GameTask.AutoPathing.Handler; using BetterGenshinImpact.GameTask.AutoWood; +using BetterGenshinImpact.GameTask.Common.Job; +using BetterGenshinImpact.GameTask.Model.GameUI; using BetterGenshinImpact.Helpers; using BetterGenshinImpact.ViewModel.Pages; using Microsoft.ClearScript; using Microsoft.Extensions.Logging; using System; using System.Threading; -using Microsoft.ClearScript; -using BetterGenshinImpact.Helpers; using System.Threading.Tasks; namespace BetterGenshinImpact.Core.Script.Dependence; @@ -23,7 +23,7 @@ public class Dispatcher { private readonly ILogger _logger = App.GetLogger(); - private object _config = null; + private readonly object _config; public Dispatcher(object config) { @@ -111,7 +111,7 @@ public class Dispatcher /// 自定义取消令牌,允许从JS控制任务取消 /// /// - public async Task RunTask(SoloTask soloTask, CancellationToken? customCt = null) + public async Task RunTask(SoloTask soloTask, CancellationToken? customCt = null) { if (soloTask == null) { @@ -140,119 +140,129 @@ public class Dispatcher // 根据名称执行任务 switch (soloTask.Name) { - case "AutoGeniusInvokation": - string content; + case "AutoGeniusInvokation": + string content; // 检查是否有自定义策略内容 - if (soloTask.Config != null) - { - var jsObject = (ScriptObject)soloTask.Config; - content = ScriptObjectConverter.GetValue(jsObject, "strategy", ""); - if (string.IsNullOrEmpty(content)) - { + if (soloTask.Config != null) + { + var jsObject = (ScriptObject)soloTask.Config; + content = ScriptObjectConverter.GetValue(jsObject, "strategy", ""); + if (string.IsNullOrEmpty(content)) + { // 回退到原有逻辑 - if (taskSettingsPageViewModel.GetTcgStrategy(out content)) - { - return; - } - } - } - else - { + if (taskSettingsPageViewModel.GetTcgStrategy(out content)) + { + return null; + } + } + } + else + { // 回退到原有逻辑 - if (taskSettingsPageViewModel.GetTcgStrategy(out content)) - { - return; - } - } - - await new AutoGeniusInvokationTask(new GeniusInvokationTaskParam(content)).Start(cancellationToken); - break; + if (taskSettingsPageViewModel.GetTcgStrategy(out content)) + { + return null; + } + } + + await new AutoGeniusInvokationTask(new GeniusInvokationTaskParam(content)).Start(cancellationToken); + return null; case "AutoWood": await new AutoWoodTask(new WoodTaskParam(taskSettingsPageViewModel.AutoWoodRoundNum, taskSettingsPageViewModel.AutoWoodDailyMaxCount)).Start(cancellationToken); - break; + return null; case "AutoFight": await new AutoFightHandler().RunAsyncByScript(cancellationToken, null, _config); - break; + return null; case "AutoDomain": if (taskSettingsPageViewModel.GetFightStrategy(out var path)) { - return; + return null; } await new AutoDomainTask(new AutoDomainParam(0, path)).Start(cancellationToken); - break; + return null; case "AutoFishing": await new AutoFishingTask(AutoFishingTaskParam.BuildFromSoloTaskConfig(soloTask.Config)).Start( cancellationToken); - break; + return null; case "AutoEat": - string? foodName = soloTask.Config == null ? null : ScriptObjectConverter.GetValue((ScriptObject)soloTask.Config, "foodName", null); - FoodEffectType? foodEffectType = soloTask.Config == null ? null : (FoodEffectType?)ScriptObjectConverter.GetValue((ScriptObject)soloTask.Config, "foodEffectType", null); - - if (foodName != null && foodEffectType != null) { - throw new NotSupportedException("不能同时指定foodName和foodEffectType"); - } + string? foodName = soloTask.Config == null ? null : ScriptObjectConverter.GetValue((ScriptObject)soloTask.Config, "foodName", null); + FoodEffectType? foodEffectType = soloTask.Config == null ? null : (FoodEffectType?)ScriptObjectConverter.GetValue((ScriptObject)soloTask.Config, "foodEffectType", null); - if (foodName == null) - { - if (foodEffectType != null) + if (foodName != null && foodEffectType != null) { - PathingPartyConfig? pathingPartyConfig = _config as PathingPartyConfig; - if (pathingPartyConfig == null) + throw new NotSupportedException("不能同时指定foodName和foodEffectType"); + } + + if (foodName == null) + { + if (foodEffectType != null) { - throw new NotSupportedException("foodEffectType参数需要调度器配置,请在调度器下使用"); - } - else - { - switch (foodEffectType) + PathingPartyConfig? pathingPartyConfig = _config as PathingPartyConfig; + if (pathingPartyConfig == null) { - case FoodEffectType.ATKBoostingDish: - foodName = pathingPartyConfig.AutoEatConfig.DefaultAtkBoostingDishName; - if (foodName == null) - { - _logger.LogInformation("缺少{Text}配置,跳过吃Buff", "默认的攻击类料理"); - return; - } - break; - case FoodEffectType.AdventurersDish: - foodName = pathingPartyConfig.AutoEatConfig.DefaultAdventurersDishName; - if (foodName == null) - { - _logger.LogInformation("缺少{Text}配置,跳过吃Buff", "默认的冒险类料理"); - return; - } - break; - case FoodEffectType.DEFBoostingDish: - foodName = pathingPartyConfig.AutoEatConfig.DefaultDefBoostingDishName; - if (foodName == null) - { - _logger.LogInformation("缺少{Text}配置,跳过吃Buff", "默认的防御类料理"); - return; - } - break; - default: - throw new NotSupportedException("JS脚本入参错误:错误的foodEffectType"); + throw new NotSupportedException("foodEffectType参数需要调度器配置,请在调度器下使用"); + } + else + { + switch (foodEffectType) + { + case FoodEffectType.ATKBoostingDish: + foodName = pathingPartyConfig.AutoEatConfig.DefaultAtkBoostingDishName; + if (foodName == null) + { + _logger.LogInformation("缺少{Text}配置,跳过吃Buff", "默认的攻击类料理"); + return null; + } + break; + case FoodEffectType.AdventurersDish: + foodName = pathingPartyConfig.AutoEatConfig.DefaultAdventurersDishName; + if (foodName == null) + { + _logger.LogInformation("缺少{Text}配置,跳过吃Buff", "默认的冒险类料理"); + return null; + } + break; + case FoodEffectType.DEFBoostingDish: + foodName = pathingPartyConfig.AutoEatConfig.DefaultDefBoostingDishName; + if (foodName == null) + { + _logger.LogInformation("缺少{Text}配置,跳过吃Buff", "默认的防御类料理"); + return null; + } + break; + default: + throw new NotSupportedException("JS脚本入参错误:错误的foodEffectType"); + } } } } + + var autoEatConfig = TaskContext.Instance().Config.AutoEatConfig; + return await new AutoEatTask(new AutoEatParam() + { + CheckInterval = autoEatConfig.CheckInterval, + EatInterval = autoEatConfig.EatInterval, + ShowNotification = autoEatConfig.ShowNotification, + FoodName = foodName + }).Start(cancellationToken); } - - var autoEatConfig = TaskContext.Instance().Config.AutoEatConfig; - await new AutoEatTask(new AutoEatParam() + case "CountInventoryItem": { - CheckInterval = autoEatConfig.CheckInterval, - EatInterval = autoEatConfig.EatInterval, - ShowNotification = autoEatConfig.ShowNotification, - FoodName = foodName - }).Start(cancellationToken); - - break; + if (soloTask.Config == null) + { + 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); + } default: throw new ArgumentException($"未知的任务名称: {soloTask.Name}", nameof(soloTask.Name)); } diff --git a/BetterGenshinImpact/Core/Script/EngineExtend.cs b/BetterGenshinImpact/Core/Script/EngineExtend.cs index 85239c75..22a3f397 100644 --- a/BetterGenshinImpact/Core/Script/EngineExtend.cs +++ b/BetterGenshinImpact/Core/Script/EngineExtend.cs @@ -57,7 +57,12 @@ public class EngineExtend engine.AddHostType("Region", typeof(Region)); engine.AddHostType("CombatScenes", typeof(CombatScenes)); - engine.AddHostType("CombatScenes", typeof(Avatar)); + engine.AddHostType("Avatar", typeof(Avatar)); + + + engine.AddHostObject("OpenCvSharp", new HostTypeCollection("OpenCvSharp")); + + // 添加C#的类型 diff --git a/BetterGenshinImpact/Core/Script/Project/Manifest.cs b/BetterGenshinImpact/Core/Script/Project/Manifest.cs index ca13e47e..0aa31798 100644 --- a/BetterGenshinImpact/Core/Script/Project/Manifest.cs +++ b/BetterGenshinImpact/Core/Script/Project/Manifest.cs @@ -7,6 +7,8 @@ using BetterGenshinImpact.Core.Config; using BetterGenshinImpact.GameTask.Common; using BetterGenshinImpact.Model; using Microsoft.Extensions.Logging; +using System.Text.Json.Serialization; +using System.Linq; namespace BetterGenshinImpact.Core.Script.Project; @@ -77,4 +79,21 @@ public class Manifest return settingItems; } + + [JsonIgnore] + public string ShortDescription + { + get + { + var lines = this.Description.Split('\n'); + if (lines.Length > 6) + { + return String.Join('\n', lines.Take(6).Append("……")); + } + else + { + return this.Description; + } + } + } } diff --git a/BetterGenshinImpact/Core/Script/ScriptRepoUpdater.cs b/BetterGenshinImpact/Core/Script/ScriptRepoUpdater.cs index 419b8665..59766726 100644 --- a/BetterGenshinImpact/Core/Script/ScriptRepoUpdater.cs +++ b/BetterGenshinImpact/Core/Script/ScriptRepoUpdater.cs @@ -18,6 +18,7 @@ using System.Linq; using System.Net.Http; using System.Threading.Tasks; using System.Windows; +using Windows.UI.Xaml.Automation; using BetterGenshinImpact.View.Windows; using LibGit2Sharp; using LibGit2Sharp.Handlers; @@ -120,9 +121,7 @@ public class ScriptRepoUpdater : Singleton // 如果仓库不存在,执行浅克隆操作 _logger.LogInformation($"浅克隆仓库: {repoUrl} 到 {repoPath}"); - CloneRepository(repoUrl, repoPath, onCheckoutProgress); - - // CloneRepository(repoUrl, repoPath); + CloneRepository(repoUrl, repoPath, "release", onCheckoutProgress); updated = true; } else @@ -142,68 +141,76 @@ public class ScriptRepoUpdater : Singleton _logger.LogWarning(ex, "备份repo.json失败,继续更新仓库"); } + // 检查是否为有效的Git仓库 + if (!Repository.IsValid(repoPath)) + { + Toast.Error($"不是有效的Git仓库,将重新克隆"); + UIDispatcherHelper.Invoke(() => Toast.Error("不是有效的Git仓库,将重新克隆")); + CloneRepository(repoUrl, repoPath, "release", onCheckoutProgress); + updated = true; + return; + } + using var repo = new Repository(repoPath); // 检查远程URL是否需要更新 var origin = repo.Network.Remotes["origin"]; if (origin.Url != repoUrl) { - // 远程URL已更改,需要更新 - _logger.LogInformation($"更新远程URL: 从 {origin.Url} 到 {repoUrl}"); - repo.Network.Remotes.Update("origin", r => r.Url = repoUrl); + // 远程URL已更改,需要删除重新克隆 + _logger.LogInformation($"远程URL已更改: 从 {origin.Url} 到 {repoUrl},将重新克隆"); + CloneRepository(repoUrl, repoPath, "release", onCheckoutProgress); + updated = true; + return; } - // 获取远程分支信息 + // 获取远程分支信息并拉取最新数据 var remote = repo.Network.Remotes["origin"]; var refSpecs = remote.FetchRefSpecs.Select(x => x.Specification); - // 使用浅拉取选项 - var fetchOptions = new FetchOptions(); - fetchOptions.ProxyOptions.ProxyType = ProxyType.None; + var fetchOptions = new FetchOptions + { + ProxyOptions = { ProxyType = ProxyType.None } + }; Commands.Fetch(repo, remote.Name, refSpecs, fetchOptions, "拉取最新更新"); - // 获取当前分支 - var branch = repo.Branches["refs/heads/origin/main"] ?? repo.Branches["main"]; - if (branch == null) - { - throw new Exception("未找到main或master分支"); - } - - // 如果是本地分支,需要设置上游分支 - if (!branch.IsRemote) - { - var trackingBranch = repo.Branches[$"origin/{branch.FriendlyName}"]; - if (trackingBranch != null && branch.TrackedBranch == null) - { - branch = repo.Branches.Update(branch, - b => b.TrackedBranch = trackingBranch.CanonicalName); - } - } - - // 检查是否有更新 + // 获取本地和远程commit var currentCommitSha = repo.Head.Tip.Sha; - // 合并或重置到最新 - if (branch.TrackedBranch != null) + // 获取远程release分支的最新commit + var remoteBranch = repo.Branches["refs/remotes/origin/release"]; + if (remoteBranch == null) { - var trackingBranch = branch.TrackedBranch; - var mergeResult = Commands.Pull( - repo, - new Signature("BetterGI", "auto@bettergi.com", DateTimeOffset.Now), - new PullOptions()); + throw new Exception("未找到远程release分支"); + } - // 检查是否有更新 - updated = currentCommitSha != repo.Head.Tip.Sha; + var remoteCommitSha = remoteBranch.Tip.Sha; + + // 比较本地和远程commit + if (currentCommitSha == remoteCommitSha) + { + _logger.LogInformation("本地仓库已是最新版本,无需更新"); + updated = false; + } + else + { + _logger.LogInformation($"检测到远程更新: 本地 {currentCommitSha[..7]} -> 远程 {remoteCommitSha[..7]},将重新克隆"); + + // commit不一致,删除本地仓库重新克隆 + CloneRepository(repoUrl, repoPath, "release", onCheckoutProgress); + updated = true; } } } catch (Exception ex) { _logger.LogError(ex, "Git仓库更新失败"); - throw; + UIDispatcherHelper.Invoke(() => Toast.Error("脚本仓库更新异常,直接删除后重新克隆\n原因:" + ex.Message)); + CloneRepository(repoUrl, repoPath, "release", onCheckoutProgress); } }); + // 如果仓库有更新,则标记新repo.json中的更新节点 if (updated) { @@ -214,12 +221,12 @@ public class ScriptRepoUpdater : Singleton if (newRepoJsonPath != null) { 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 @@ -355,7 +362,6 @@ public class ScriptRepoUpdater : Singleton } - /// /// 解析lastUpdated时间戳 /// @@ -384,6 +390,7 @@ public class ScriptRepoUpdater : Singleton { var options = new CloneOptions { + BranchName = "release", // 指定分支 Checkout = true, IsBare = false, RecurseSubmodules = false, // 不递归克隆子模块 @@ -396,54 +403,45 @@ public class ScriptRepoUpdater : Singleton /// /// - /// 相当于 Repository.Clone(repoUrl, repoPath, options); + /// 相当于 Repository.Clone(repoUrl, repoPath, options); + /// 用这个方法可以无视本地代理 /// /// /// + /// /// /// - private void CloneRepository(string repoUrl, string repoPath, CheckoutProgressHandler? onCheckoutProgress) + private void CloneRepository(string repoUrl, string repoPath, string branchName, CheckoutProgressHandler? onCheckoutProgress) { - // 1. 创建目录 + DirectoryHelper.DeleteReadOnlyDirectory(repoPath); Directory.CreateDirectory(repoPath); - - // 2. 初始化 Git 仓库 Repository.Init(repoPath); using var repo = new Repository(repoPath); GitConfig(repo); - // 3. 添加远程源 + // 添加远程源 Remote remote = repo.Network.Remotes.Add("origin", repoUrl); - // 4. 获取数据(使用浅克隆选项) + // 只拉取指定分支 var fetchOptions = new FetchOptions { - TagFetchMode = TagFetchMode.None // 不获取标签 + TagFetchMode = TagFetchMode.None, + ProxyOptions = { ProxyType = ProxyType.None } }; - fetchOptions.ProxyOptions.ProxyType = ProxyType.None; + string refSpec = $"+refs/heads/{branchName}:refs/remotes/origin/{branchName}"; + Commands.Fetch(repo, remote.Name, new[] { refSpec }, fetchOptions, "初始化拉取"); - // 5. 执行获取操作 - Commands.Fetch(repo, remote.Name, ["+refs/heads/*:refs/remotes/origin/*"], fetchOptions, "初始化拉取"); - - // 6. 创建本地分支并跟踪远程分支 - var remoteBranch = repo.Branches["refs/remotes/origin/main"] ?? repo.Branches["refs/remotes/origin/master"]; + // 获取远程分支 + var remoteBranch = repo.Branches[$"refs/remotes/origin/{branchName}"]; if (remoteBranch == null) - { - throw new Exception("远程仓库中未找到 main 或 master 分支"); - } + throw new Exception($"远程仓库中未找到 {branchName} 分支"); - // 7. 创建并检出本地分支 - var localBranch = repo.CreateBranch(remoteBranch.FriendlyName, remoteBranch.Tip); - - // 8. 设置本地分支跟踪远程分支 + // 创建并检出本地分支 + var localBranch = repo.CreateBranch(branchName, remoteBranch.Tip); repo.Branches.Update(localBranch, b => b.TrackedBranch = remoteBranch.CanonicalName); - // 9. 检出分支 - CheckoutOptions checkoutOptions = new CheckoutOptions - { - OnCheckoutProgress = onCheckoutProgress - }; + var checkoutOptions = new CheckoutOptions { OnCheckoutProgress = onCheckoutProgress }; Commands.Checkout(repo, localBranch, checkoutOptions); } @@ -795,14 +793,14 @@ public class ScriptRepoUpdater : Singleton { var scriptPath = Path.Combine(repoPath, path); var destPath = Path.Combine(userPath, remainingPath); - + // 备份需要保存的文件 List backupFiles = new List(); if (first == "js") // 只对JS脚本进行备份 { backupFiles = BackupScriptFiles(path, repoPath); } - + if (Directory.Exists(scriptPath)) { if (Directory.Exists(destPath)) @@ -827,7 +825,7 @@ public class ScriptRepoUpdater : Singleton File.Copy(scriptPath, destPath, true); } - + // 恢复备份的文件 if (first == "js" && backupFiles.Count > 0) // 只对JS脚本进行恢复 { @@ -891,7 +889,7 @@ public class ScriptRepoUpdater : Singleton { var jsonContent = File.ReadAllText(repoJsonPath); var jsonObj = JObject.Parse(jsonContent); - + if (jsonObj["indexes"] is JArray indexes) { // 递归收集所有路径 @@ -915,7 +913,7 @@ public class ScriptRepoUpdater : Singleton } } } - + CollectPaths(indexes, ""); } } @@ -925,7 +923,7 @@ public class ScriptRepoUpdater : Singleton { // 构建父子关系映射,只记录直接子节点 var parentChildMap = new Dictionary>(); - + // 遍历所有路径,找到每个节点的父节点 foreach (var path in allAvailablePaths) { @@ -937,31 +935,31 @@ public class ScriptRepoUpdater : Singleton { parentChildMap[parentPath] = new List(); } - + if (!parentChildMap[parentPath].Contains(path)) { parentChildMap[parentPath].Add(path); } } } - + // 递归检查父节点,直到没有新的父节点需要添加 bool hasNewPaths; do { hasNewPaths = false; var pathsToAdd = new HashSet(); - + // 检查每个父节点 foreach (var kvp in parentChildMap) { var parentPath = kvp.Key; var directChildren = kvp.Value; - + // 检查所有直接子节点是否都已被订阅 - bool allDirectChildrenSubscribed = directChildren.All(child => + bool allDirectChildrenSubscribed = directChildren.All(child => pathsToKeep.Contains(child)); - + // 如果所有直接子节点都已被订阅,且父节点本身未被订阅,则添加父节点 if (allDirectChildrenSubscribed && !pathsToKeep.Contains(parentPath)) { @@ -969,7 +967,7 @@ public class ScriptRepoUpdater : Singleton hasNewPaths = true; } } - + // 将需要添加的父节点加入订阅列表 foreach (var pathToAdd in pathsToAdd) { @@ -1103,18 +1101,18 @@ public class ScriptRepoUpdater : Singleton private List GetMatchedFiles(string basePath, string pattern) { var matchedFiles = new List(); - + try { // 检查是否是正则表达式(以^开头或包含特殊字符) bool isRegex = pattern.StartsWith("^") || pattern.Contains(".*") || pattern.Contains("\\d") || pattern.Contains("\\w"); - + if (isRegex) { // 使用正则表达式匹配 var regex = new System.Text.RegularExpressions.Regex(pattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase); var allFiles = Directory.GetFiles(basePath, "*", SearchOption.AllDirectories); - + foreach (var file in allFiles) { var relativePath = Path.GetRelativePath(basePath, file); @@ -1129,7 +1127,7 @@ public class ScriptRepoUpdater : Singleton // 使用通配符匹配 var searchPattern = Path.GetFileName(pattern); var searchDir = Path.GetDirectoryName(pattern); - + if (string.IsNullOrEmpty(searchDir)) { // 只在当前目录搜索 @@ -1152,7 +1150,7 @@ public class ScriptRepoUpdater : Singleton { _logger.LogError(ex, $"获取匹配文件时发生错误: {pattern}"); } - + return matchedFiles; } @@ -1215,6 +1213,7 @@ public class ScriptRepoUpdater : Singleton savedFile += "/"; isDir = true; } + if (isDir) { var dirPath = Path.Combine(scriptUserPath, savedFile.TrimEnd('/', '\\')); @@ -1241,6 +1240,7 @@ public class ScriptRepoUpdater : Singleton { Directory.CreateDirectory(backupFileDir); } + try { File.Copy(matchedFile, backupFilePath, true); @@ -1251,6 +1251,7 @@ public class ScriptRepoUpdater : Singleton _logger.LogError(ex, $"备份文件失败: {matchedFile}"); } } + if (matchedFiles.Count == 0) { _logger.LogWarning($"没有找到匹配的文件: {savedFile}"); @@ -1262,6 +1263,7 @@ public class ScriptRepoUpdater : Singleton { _logger.LogError(ex, $"备份脚本文件时发生错误: {scriptPath}"); } + return backupFiles; } @@ -1302,6 +1304,7 @@ public class ScriptRepoUpdater : Singleton _logger.LogWarning($"未知的脚本路径映射: {scriptPath}"); return; } + var scriptUserPath = Path.Combine(userPath, remainingPath); // 还原所有备份文件 @@ -1316,6 +1319,7 @@ public class ScriptRepoUpdater : Singleton { Directory.CreateDirectory(restoreDir); } + try { File.Copy(file, restorePath, true); @@ -1349,4 +1353,4 @@ public class ScriptRepoUpdater : Singleton _logger.LogError(ex, $"恢复脚本文件时发生错误: {scriptPath}"); } } -} +} \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/AutoArtifactSalvage/ArtifactStat.cs b/BetterGenshinImpact/GameTask/AutoArtifactSalvage/ArtifactStat.cs index 214c6887..9a31e297 100644 --- a/BetterGenshinImpact/GameTask/AutoArtifactSalvage/ArtifactStat.cs +++ b/BetterGenshinImpact/GameTask/AutoArtifactSalvage/ArtifactStat.cs @@ -1,3 +1,5 @@ +using System.Text; + namespace BetterGenshinImpact.GameTask.AutoArtifactSalvage { /// @@ -34,5 +36,24 @@ namespace BetterGenshinImpact.GameTask.AutoArtifactSalvage public int Level { get; private set; } // PS:圣遗物的种类和品质在点击查看之前就可以通过识别图标获悉,所以不必在此模型类中获取 + + /// + /// 生成一个手工拼接的成员结构示意字符串 + /// + /// + public string ToStructuredString() + { + StringBuilder sb = new StringBuilder(); + sb.Append("Properties").Append('\n'); + sb.Append("├─").Append("Name: ").Append(this.Name).Append('\n'); + sb.Append("├─").Append("MainAffix: ").Append(this.MainAffix.Type).Append(", ").Append(this.MainAffix.Value).Append('\n'); + sb.Append("├─").Append("MinorAffixes: ").Append('\n'); + for (int i = 0; i < this.MinorAffixes.Length; i++) + { + sb.Append('│').Append('\t').Append(i == this.MinorAffixes.Length - 1 ? "└─" : "├─").Append($"[{i}]: ").Append(this.MinorAffixes[i].Type).Append(", ").Append(this.MinorAffixes[i].Value).Append('\n'); + } + sb.Append("└─").Append("Level: ").Append(this.Level); + return sb.ToString(); + } } } diff --git a/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageConfig.cs b/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageConfig.cs index 28a61138..5762ca55 100644 --- a/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageConfig.cs +++ b/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageConfig.cs @@ -8,12 +8,15 @@ public partial class AutoArtifactSalvageConfig : ObservableObject { // JavaScript [ObservableProperty] - private string _javaScript = - @"(async function (artifact) { - var hasATK = Array.from(artifact.MinorAffixes).some(affix => affix.Type == 'ATK'); - var hasDEF = Array.from(artifact.MinorAffixes).some(affix => affix.Type == 'DEF'); - Output = hasATK && hasDEF; - })(ArtifactStat);"; + private string _javaScript = @"(async function (artifact) { + var hasATK = Array.from(artifact.MinorAffixes).some(affix => affix.Type == 'ATK'); + var hasDEF = Array.from(artifact.MinorAffixes).some(affix => affix.Type == 'DEF'); + Output = hasATK && hasDEF; +})(ArtifactStat);"; + + // JavaScript + [ObservableProperty] + private string _artifactSetFilter = ""; // 正则表达式 [Obsolete] @@ -28,4 +31,8 @@ public partial class AutoArtifactSalvageConfig : ObservableObject // 最多检查多少个圣遗物 [ObservableProperty] private int _maxNumToCheck = 100; + + // 单次识别失败政策 + [ObservableProperty] + private RecognitionFailurePolicy _recognitionFailurePolicy = RecognitionFailurePolicy.Skip; } \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageTask.cs b/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageTask.cs index f5289b88..5207c3f0 100644 --- a/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageTask.cs +++ b/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageTask.cs @@ -7,6 +7,7 @@ using BetterGenshinImpact.GameTask.Common; using BetterGenshinImpact.GameTask.Common.BgiVision; using BetterGenshinImpact.GameTask.Common.Element.Assets; using BetterGenshinImpact.GameTask.Common.Job; +using BetterGenshinImpact.GameTask.GetGridIcons; using BetterGenshinImpact.GameTask.Model.Area; using BetterGenshinImpact.GameTask.Model.GameUI; using BetterGenshinImpact.Helpers; @@ -16,8 +17,10 @@ using Microsoft.ClearScript; using Microsoft.ClearScript.V8; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; +using Microsoft.ML.OnnxRuntime; using OpenCvSharp; using System; +using System.Collections.Frozen; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; @@ -35,7 +38,7 @@ namespace BetterGenshinImpact.GameTask.AutoArtifactSalvage; /// public class AutoArtifactSalvageTask : ISoloTask { - private readonly ILogger logger = App.GetLogger(); + private readonly ILogger logger; private readonly InputSimulator input = Simulation.SendInput; private CancellationToken ct; @@ -50,19 +53,28 @@ public class AutoArtifactSalvageTask : ISoloTask private readonly string? javaScript; + private readonly string? artifactSetFilter; + private readonly int? maxNumToCheck; + private readonly RecognitionFailurePolicy? recognitionFailurePolicy; + private readonly bool returnToMainUi = true; - private readonly CultureInfo cultureInfo; + private readonly CultureInfo? cultureInfo; - public AutoArtifactSalvageTask(int star, string? javaScript = null, int? maxNumToCheck = null) + private readonly FrozenDictionary artifactAffixStrDic; + + public AutoArtifactSalvageTask(AutoArtifactSalvageTaskParam param, ILogger? logger = null) { - this.star = star; - this.javaScript = javaScript; - this.maxNumToCheck = maxNumToCheck; - IStringLocalizer stringLocalizer = App.GetService>() ?? throw new NullReferenceException(); - this.cultureInfo = new CultureInfo(TaskContext.Instance().Config.OtherConfig.GameCultureInfoName); + this.star = param.Star; + this.javaScript = param.JavaScript; + this.artifactSetFilter = param.ArtifactSetFilter; + this.maxNumToCheck = param.MaxNumToCheck; + this.recognitionFailurePolicy = param.RecognitionFailurePolicy; + this.logger = logger ?? App.GetLogger(); + var stringLocalizer = param.StringLocalizer ?? App.GetService>() ?? throw new NullReferenceException(); + this.cultureInfo = param.GameCultureInfo; quickSelectLocalizedString = stringLocalizer.WithCultureGet(cultureInfo, "快速选择"); numOfStarLocalizedString = [ @@ -71,14 +83,11 @@ public class AutoArtifactSalvageTask : ISoloTask stringLocalizer.WithCultureGet(cultureInfo, "3星圣遗物"), stringLocalizer.WithCultureGet(cultureInfo, "4星圣遗物") ]; + + artifactAffixStrDic = ArtifactAffix.DefaultStrDic.Select(kvp => new KeyValuePair(kvp.Key, stringLocalizer.WithCultureGet(cultureInfo, kvp.Value))).ToFrozenDictionary(); } - public AutoArtifactSalvageTask(int star, bool returnToMainUi) : this(star) - { - this.returnToMainUi = returnToMainUi; - } - - public static async Task OpenBag(GridScreenName gridScreenName, InputSimulator input, ILogger logger, CancellationToken ct) + public static async Task OpenInventory(GridScreenName gridScreenName, InputSimulator input, ILogger logger, CancellationToken ct) { RecognitionObject? recognitionObjectChecked; RecognitionObject? recognitionObjectUnchecked; @@ -175,7 +184,7 @@ public class AutoArtifactSalvageTask : ISoloTask await new ReturnMainUiTask().Start(ct); } - await OpenBag(GridScreenName.Artifacts, this.input, this.logger, this.ct); + await OpenInventory(GridScreenName.Artifacts, this.input, this.logger, this.ct); // 点击分解按钮打开分解界面 using var ra2 = CaptureToRectArea(); @@ -224,7 +233,11 @@ public class AutoArtifactSalvageTask : ISoloTask } } - Bv.ClickWhiteConfirmButton(ra4); + using var quickSelectConfirmBtn = ra4.Find(ElementAssets.Instance.BtnWhiteConfirm); + if (quickSelectConfirmBtn.IsExist()) + { + quickSelectConfirmBtn.Click(); + } await Delay(1500, ct); @@ -260,7 +273,39 @@ public class AutoArtifactSalvageTask : ISoloTask // 分解5星 if (javaScript != null) { - await Salvage5Star(this.javaScript, this.maxNumToCheck ?? throw new ArgumentException($"{nameof(this.maxNumToCheck)}不能为空")); + if (!string.IsNullOrWhiteSpace(this.artifactSetFilter)) + { + // 其实是点击筛选按钮……快速选择确认的这个按钮正好和筛选按钮位置重合,摆烂直接用了 + quickSelectConfirmBtn.Click(); + await Delay(400, ct); + // 点击所属套装 + ra5.ClickTo(315, 205); + await Delay(1000, ct); + // 遍历套装Grid勾选套装 + using InferenceSession session = GridIconsAccuracyTestTask.LoadModel(out Dictionary prototypes); + ArtifactSetFilterScreen gridScreen = new ArtifactSetFilterScreen(new GridParams(new Rect(40, 100, 1300, 852), 2, 3, 40, 40, 0.024), this.logger, this.ct); + await foreach (ImageRegion itemRegion in gridScreen) + { + using Mat img125 = GetGridIconsTask.CropResizeArtifactSetFilterGridIcon(itemRegion); + (string predName, _) = GridIconsAccuracyTestTask.Infer(img125, session, prototypes); + if (this.artifactSetFilter.Contains(predName)) + { + itemRegion.Click(); + await Delay(100, ct); + } + } + // 点击确认筛选 + using var confirmFilterBtnRegion = CaptureToRectArea(); + Bv.ClickWhiteConfirmButton(confirmFilterBtnRegion); + await Delay(1500, ct); + // 点击确认 + using var confirmBtnRegion = CaptureToRectArea(); + Bv.ClickWhiteConfirmButton(confirmBtnRegion); + await Delay(600, ct); + } + + // 逐一点选查看面板筛选 + await Salvage5Star(); logger.LogInformation("筛选完毕,请复查并手动分解"); } else @@ -274,14 +319,14 @@ public class AutoArtifactSalvageTask : ISoloTask } } - private async Task Salvage5Star(string javaScript, int maxNumToCheck) + private async Task Salvage5Star() { - int count = maxNumToCheck; + string javaScript = this.javaScript ?? throw new ArgumentException($"{nameof(this.javaScript)}不能为空"); + int count = this.maxNumToCheck ?? throw new ArgumentException($"{nameof(this.maxNumToCheck)}不能为空"); + RecognitionFailurePolicy recognitionFailurePolicy = this.recognitionFailurePolicy ?? throw new ArgumentException($"{nameof(this.recognitionFailurePolicy)}不能为空"); - using var ra0 = CaptureToRectArea(); - GridScreenParams gridParams = GridScreenParams.Templates[GridScreenName.ArtifactSalvage]; - Rect gridRoi = gridParams.GetRect(ra0); - GridScreen gridScreen = new GridScreen(gridRoi, gridParams, this.logger, this.ct); // 圣遗物分解Grid有4行9列 + GridParams gridParams = GridParams.Templates[GridScreenName.ArtifactSalvage]; + GridScreen gridScreen = new GridScreen(gridParams, this.logger, this.ct); // 圣遗物分解Grid有4行9列 await foreach (ImageRegion itemRegion in gridScreen) { Rect gridRect = itemRegion.ToRect(); @@ -291,11 +336,31 @@ public class AutoArtifactSalvageTask : ISoloTask await Delay(300, ct); using var ra1 = CaptureToRectArea(); - using ImageRegion itemRegion1 = ra1.DeriveCrop(gridRect + new Point(gridRoi.X, gridRoi.Y)); + using ImageRegion itemRegion1 = ra1.DeriveCrop(gridRect + new Point(gridParams.Roi.X, gridParams.Roi.Y)); if (GetArtifactStatus(itemRegion1.SrcMat) == ArtifactStatus.Selected) { - using ImageRegion card = ra1.DeriveCrop(new Rect((int)(ra1.Width * 0.70), (int)(ra1.Width * 0.055), (int)(ra1.Width * 0.24), (int)(ra1.Width * 0.29))); - ArtifactStat artifact = GetArtifactStat(card.SrcMat, OcrFactory.Paddle, this.cultureInfo, out string allText); + using ImageRegion card = ra1.DeriveCrop(new Rect((int)(ra1.Width * 0.70), (int)(ra1.Height * 0.112), (int)(ra1.Width * 0.275), (int)(ra1.Height * 0.50))); + + ArtifactStat artifact; + try + { + artifact = GetArtifactStat(card.SrcMat, OcrFactory.Paddle, out string allText); + } + catch (Exception e) + { + if (recognitionFailurePolicy == RecognitionFailurePolicy.Skip) + { + logger.LogError("识别失败,跳过当前圣遗物:{msg}", e.Message); + + itemRegion.Click(); // 反选取消 + await Delay(100, ct); + continue; + } + else + { + throw; + } + } if (IsMatchJavaScript(artifact, javaScript)) { @@ -379,35 +444,78 @@ public class AutoArtifactSalvageTask : ISoloTask return match.Success; } - public static ArtifactStat GetArtifactStat(Mat src, IOcrService ocrService, CultureInfo cultureInfo, out string allText) + public ArtifactStat GetArtifactStat(Mat src, IOcrService ocrService, out string allText) { - var ocrResult = ocrService.OcrResult(src); - allText = ocrResult.Text; - var lines = ocrResult.Text.Split('\n'); + using Mat gray = src.CvtColor(ColorConversionCodes.BGR2GRAY); + Mat hatKernel = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(15, 15)/*需根据实际文本大小调整*/); // 顶帽运算核 + + Mat nameRoi = gray.SubMat(new Rect(0, 0, src.Width, (int)(src.Height * 0.106))); + //Cv2.ImShow("name", nameRoi); + Mat typeRoi = gray.SubMat(new Rect(0, (int)(src.Height * 0.106), src.Width, (int)(src.Height * 0.106))); + #region 主词条预处理 去除背景干扰 + Mat mainAffixRoi = gray.SubMat(new Rect(0, (int)(src.Height * 0.22), (int)(src.Width * 0.55), (int)(src.Height * 0.30))); + using Mat mainAffixRoiBottomHat = mainAffixRoi.MorphologyEx(MorphTypes.TopHat, hatKernel); + using Mat mainAffixRoiThreshold = mainAffixRoiBottomHat.Threshold(30, 255, ThresholdTypes.Binary); + //Cv2.ImShow("mainAffix", mainAffixRoiThreshold); + #endregion + #region 副词条预处理 还是不处理效果最好…… + Mat levelAndMinorAffixRoi = gray.SubMat(new Rect(0, (int)(src.Height * 0.52), src.Width, (int)(src.Height * 0.48))); + //using Mat levelAndMinorAffixRoiThreshold = new Mat(); + //double otsu = Cv2.Threshold(levelAndMinorAffixRoi, levelAndMinorAffixRoiThreshold, 0, 255, ThresholdTypes.Binary | ThresholdTypes.Otsu); + // //using Mat levelAndMinorAffixRoiThreshold = levelAndMinorAffixRoi.Threshold(170, 255, ThresholdTypes.Binary); + //Cv2.ImShow($"levelAndMinorAffixRoi = {otsu}", levelAndMinorAffixRoiThreshold); + #endregion + //Cv2.WaitKey(); + + var nameOcrResult = ocrService.OcrResult(nameRoi); + var typeOcrResult = ocrService.OcrResult(typeRoi); + var mainAffixOcrResult = ocrService.OcrResult(mainAffixRoiThreshold); + string mainAffixText = string.Join("\n", mainAffixOcrResult.Regions.Where(r => r.Score > 0.5).OrderBy(r => r.Rect.Center.Y).ThenBy(r => r.Rect.Center.X).Select(r => r.Text)); + var mainAffixLines = mainAffixText.Split('\n'); + var levelAndMinorAffixOcrResult = ocrService.OcrResult(levelAndMinorAffixRoi); + string levelAndMinorAffixText = string.Join("\n", levelAndMinorAffixOcrResult.Regions.Where(r => r.Score > 0.5) + .Where(r => r.Rect.BoundingRect().Left < levelAndMinorAffixRoi.Width * 0.1) // 一定是贴着左边的,排除套装效果文字也存在类似+15%的情况 + .OrderBy(r => r.Rect.Center.Y).ThenBy(r => r.Rect.Center.X).Select(r => r.Text)); + var levelAndMinorAffixLines = levelAndMinorAffixText.Split('\n'); + + allText = String.Join('\n', nameOcrResult.Text, typeOcrResult.Text, mainAffixText, levelAndMinorAffixText); + string percentStr = "%"; // 名称 - string name = lines[0]; + string name = nameOcrResult.Text; #region 主词条 - var defaultMainAffix = ArtifactAffix.DefaultStrDic.Select(kvp => kvp.Value).Distinct(); - string mainAffixTypeLine = lines.Single(l => defaultMainAffix.Contains(l)); - ArtifactAffixType mainAffixType = ArtifactAffix.DefaultStrDic.First(kvp => kvp.Value == mainAffixTypeLine).Key; - string mainAffixValueLine = lines.Select(l => + var defaultMainAffix = this.artifactAffixStrDic.Select(kvp => kvp.Value).Distinct(); + string mainAffixTypeLine = mainAffixLines.SingleOrDefault(l => defaultMainAffix.Contains(l)) ?? throw new Exception($"未找到主词条对应的行:\n{mainAffixText}"); + ArtifactAffixType mainAffixType = this.artifactAffixStrDic.First(kvp => kvp.Value == mainAffixTypeLine).Key; + string mainAffixValueLine = mainAffixLines.Select(l => { - string pattern = @"^(\d+\.?\d*)(%?)$"; + string pattern = @"^([\d., ]*)(%?)$"; pattern = pattern.Replace("%", percentStr); // 这样一行一行写只是为了IDE能保持正则字符串高亮 Match match = Regex.Match(l, pattern); if (match.Success) { + if (mainAffixType == ArtifactAffixType.ATK && !String.IsNullOrEmpty(match.Groups[2].Value)) + { + mainAffixType = ArtifactAffixType.ATKPercent; + } + if (mainAffixType == ArtifactAffixType.DEF && !String.IsNullOrEmpty(match.Groups[2].Value)) + { + mainAffixType = ArtifactAffixType.DEFPercent; + } + if (mainAffixType == ArtifactAffixType.HP && !String.IsNullOrEmpty(match.Groups[2].Value)) + { + mainAffixType = ArtifactAffixType.HPPercent; + } return match.Groups[1].Value; } else { return null; } - }).Where(l => l != null).Cast().Single(); - if (!float.TryParse(mainAffixValueLine, NumberStyles.Any, cultureInfo, out float value)) + }).Where(l => l != null).Cast().SingleOrDefault() ?? throw new Exception($"未找到主词条数值对应的行:\n{mainAffixText}"); + if (!float.TryParse(mainAffixValueLine, NumberStyles.Any, this.cultureInfo, out float value)) { throw new Exception($"未识别的主词条数值:{mainAffixValueLine}"); } @@ -415,15 +523,15 @@ public class AutoArtifactSalvageTask : ISoloTask #endregion #region 副词条 - ArtifactAffix[] minorAffixes = lines.Select(l => + ArtifactAffix[] minorAffixes = levelAndMinorAffixLines.Select(l => { - string pattern = @"^[•·]?([^+]+)\+(\d+\.?\d*)(%?)$"; + string pattern = @"^[•·]?([^+::]+)\+([\d., ]*)(%?)$"; pattern = pattern.Replace("%", percentStr); Match match = Regex.Match(l, pattern); if (match.Success) { ArtifactAffixType artifactAffixType; - var dic = ArtifactAffix.DefaultStrDic; + var dic = this.artifactAffixStrDic; if (match.Groups[1].Value.Contains(dic[ArtifactAffixType.ATK])) { @@ -493,7 +601,7 @@ public class AutoArtifactSalvageTask : ISoloTask #endregion #region 等级 - string levelLine = lines.Select(l => + string levelLine = levelAndMinorAffixLines.Select(l => { string pattern = @"^\+(\d*)$"; Match match = Regex.Match(l, pattern); @@ -505,7 +613,7 @@ public class AutoArtifactSalvageTask : ISoloTask { return null; } - }).Where(l => l != null).Cast().Single(); + }).Where(l => l != null).Cast().SingleOrDefault() ?? throw new Exception($"未找到等级对应的行:\n{levelAndMinorAffixText}"); if (!int.TryParse(levelLine, out int level) || level < 0 || level > 20) { throw new Exception($"未识别的等级:{levelLine}"); diff --git a/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageTask.en.resx b/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageTask.en.resx index 2b727f12..7ee34951 100644 --- a/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageTask.en.resx +++ b/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageTask.en.resx @@ -1,4 +1,4 @@ - + - + diff --git a/BetterGenshinImpact/View/Pages/TaskSettingsPage.xaml b/BetterGenshinImpact/View/Pages/TaskSettingsPage.xaml index 3d10b165..db677611 100644 --- a/BetterGenshinImpact/View/Pages/TaskSettingsPage.xaml +++ b/BetterGenshinImpact/View/Pages/TaskSettingsPage.xaml @@ -2339,10 +2339,11 @@ + - - + + 只要满足的圣遗物都会被选中 - - JS接受ArtifactStat作为入参;应对Output赋值一个布尔值作为返回 - + + + + + + + + + + + + 按套装筛选 + + + 利用游戏自带的筛选功能先行筛选 + + 一般填写套装内生之花名,可填入多个名称;留空则不用 + + + @@ -2426,6 +2464,35 @@ Value="{Binding Config.AutoArtifactSalvageConfig.MaxNumToCheck, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Text="{Binding Config.AutoArtifactSalvageConfig.MaxNumToCheck, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> + + + + + + + + + + + + + diff --git a/BetterGenshinImpact/View/Pages/View/ScriptGroupConfigView.xaml b/BetterGenshinImpact/View/Pages/View/ScriptGroupConfigView.xaml index 69683027..f7c98998 100644 --- a/BetterGenshinImpact/View/Pages/View/ScriptGroupConfigView.xaml +++ b/BetterGenshinImpact/View/Pages/View/ScriptGroupConfigView.xaml @@ -1445,8 +1445,14 @@ + Text="" + TextWrapping="Wrap"> + 自动吃食物配置 - + + 点击查看调用方法 + + - - + + + + + + + + + diff --git a/BetterGenshinImpact/View/Windows/OcrDialog.xaml.cs b/BetterGenshinImpact/View/Windows/OcrDialog.xaml.cs index 2da7f5a7..8b32bc18 100644 --- a/BetterGenshinImpact/View/Windows/OcrDialog.xaml.cs +++ b/BetterGenshinImpact/View/Windows/OcrDialog.xaml.cs @@ -3,8 +3,12 @@ using BetterGenshinImpact.GameTask; using BetterGenshinImpact.GameTask.AutoArtifactSalvage; using BetterGenshinImpact.GameTask.Common; using BetterGenshinImpact.Helpers.Extensions; +using OpenCvSharp; +using System; using System.Globalization; +using System.IO; using System.Windows; +using System.Windows.Controls; namespace BetterGenshinImpact.View.Windows; @@ -15,6 +19,8 @@ public partial class OcrDialog private readonly double widthRatio; private readonly double heightRatio; private readonly string? javaScript; + private readonly AutoArtifactSalvageTask autoArtifactSalvageTask; + public OcrDialog(double xRatio, double yRatio, double widthRatio, double heightRatio, string title, string? javaScript = null) { this.xRatio = xRatio; @@ -22,6 +28,7 @@ public partial class OcrDialog this.widthRatio = widthRatio; this.heightRatio = heightRatio; this.javaScript = javaScript; + this.autoArtifactSalvageTask = new AutoArtifactSalvageTask(new AutoArtifactSalvageTaskParam(5, null, null, null, null, new CultureInfo(TaskContext.Instance().Config.OtherConfig.GameCultureInfoName))); InitializeComponent(); @@ -33,16 +40,46 @@ public partial class OcrDialog { using var ra = TaskControl.CaptureToRectArea(); using var card = ra.DeriveCrop(new OpenCvSharp.Rect((int)(ra.Width * xRatio), (int)(ra.Height * yRatio), (int)(ra.Width * widthRatio), (int)(ra.Height * heightRatio))); + //Cv2.ImWrite($"{DateTime.Now.ToString("yyyyMMddHHmm")}_GetArtifactStat.png", card.SrcMat); var bitmapImage = card.SrcMat.ToWriteableBitmap(); this.Screenshot.Source = bitmapImage; - ArtifactStat artifact = AutoArtifactSalvageTask.GetArtifactStat(card.SrcMat, OcrFactory.Paddle, new CultureInfo(TaskContext.Instance().Config.OtherConfig.GameCultureInfoName), out string allText); - this.TxtRecognized.Text = allText; - if (this.javaScript != null) + try { - bool isMatch = AutoArtifactSalvageTask.IsMatchJavaScript(artifact, this.javaScript); - this.RegexResult.Text = isMatch ? "匹配" : "不匹配"; + ArtifactStat artifact = this.autoArtifactSalvageTask.GetArtifactStat(card.SrcMat, OcrFactory.Paddle, out string allText); + + this.TxtRecognized.Text = allText; + this.ModelStructure.Text = artifact.ToStructuredString(); + if (this.javaScript != null) + { + bool isMatch = AutoArtifactSalvageTask.IsMatchJavaScript(artifact, this.javaScript); + this.RegexResult.Text = isMatch ? "匹配" : "不匹配"; + } + } + catch (Exception e) + { + var multilineTextBox = new TextBox + { + TextWrapping = TextWrapping.Wrap, + VerticalScrollBarVisibility = ScrollBarVisibility.Auto, + Text = e.ToString(), + IsReadOnly = true + }; + var p = new PromptDialog($"出错了:{e.Message}\r\n\r\n是否保存该圣遗物截图?(至log/autoArtifactSalvageException/)", $"异常处理", multilineTextBox, null); + p.Height = 600; + p.MaxWidth = 800; + p.ShowDialog(); + + if (p.DialogResult == true) + { + string directory = Path.Combine(AppContext.BaseDirectory, "log/autoArtifactSalvageException"); + Directory.CreateDirectory(directory); + string filePath = Path.Combine(directory, $"{DateTime.Now.ToString("yyyyMMddHHmmss")}_GetArtifactStat.png"); + Cv2.ImWrite(filePath, card.SrcMat); + } + + throw; } this.UpdateLayout(); } diff --git a/BetterGenshinImpact/View/Windows/ScriptRepoWindow.xaml b/BetterGenshinImpact/View/Windows/ScriptRepoWindow.xaml index c71ded05..b77b3d85 100644 --- a/BetterGenshinImpact/View/Windows/ScriptRepoWindow.xaml +++ b/BetterGenshinImpact/View/Windows/ScriptRepoWindow.xaml @@ -7,7 +7,7 @@ xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml" xmlns:vio="http://schemas.lepo.co/wpfui/2022/xaml/violeta" Title="脚本仓库" - Width="400" + Width="410" MinWidth="360" MinHeight="50" ResizeMode="NoResize" @@ -36,107 +36,220 @@ - + + + + + + + - - - - - - - + + + + + + + + + + + + + + + - - - - - - + + + + + + - - - + + + - - - - - - + + + + + + - - - + + + - - - - - - - + + + + + + - - - - + + + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/BetterGenshinImpact/View/Windows/ScriptRepoWindow.xaml.cs b/BetterGenshinImpact/View/Windows/ScriptRepoWindow.xaml.cs index cf15970e..06463119 100644 --- a/BetterGenshinImpact/View/Windows/ScriptRepoWindow.xaml.cs +++ b/BetterGenshinImpact/View/Windows/ScriptRepoWindow.xaml.cs @@ -5,13 +5,18 @@ using BetterGenshinImpact.Helpers; using BetterGenshinImpact.Helpers.Ui; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using Microsoft.Win32; using System; using System.Collections.ObjectModel; using System.ComponentModel; +using System.Diagnostics; using System.IO; +using System.IO.Compression; using System.Linq; +using System.Net.Http; using System.Threading.Tasks; using System.Windows; +using System.Windows.Navigation; using Wpf.Ui.Violeta.Controls; namespace BetterGenshinImpact.View.Windows; @@ -48,6 +53,9 @@ public partial class ScriptRepoWindow [ObservableProperty] private string _updateProgressText = "准备更新,请耐心等待..."; [ObservableProperty] private ScriptConfig _config = TaskContext.Instance().Config.ScriptConfig; + // 在线更新相关属性 + [ObservableProperty] private string _onlineDownloadUrl = ""; + public ScriptRepoWindow() { InitializeRepoChannels(); @@ -250,4 +258,212 @@ public partial class ScriptRepoWindow } } } + + /* + [RelayCommand] + private async Task DownloadOnlineRepo() + { + if (string.IsNullOrWhiteSpace(OnlineDownloadUrl)) + { + Toast.Warning("请输入有效的下载地址。"); + return; + } + + if (IsUpdating) + { + Toast.Warning("请等待当前操作完成后再进行下载。"); + return; + } + + try + { + IsUpdating = true; + UpdateProgressValue = 0; + UpdateProgressText = "正在下载脚本仓库..."; + + using var httpClient = new HttpClient(); + httpClient.Timeout = TimeSpan.FromMinutes(10); + + // 下载文件 + var response = await httpClient.GetAsync(OnlineDownloadUrl); + response.EnsureSuccessStatusCode(); + + var tempZipPath = Path.Combine(Path.GetTempPath(), $"script_repo_{DateTime.Now:yyyyMMddHHmmss}.zip"); + await using (var fileStream = File.Create(tempZipPath)) + { + await response.Content.CopyToAsync(fileStream); + } + + UpdateProgressText = "正在解压并导入..."; + UpdateProgressValue = 50; + + // 导入下载的zip文件 + await ImportZipFile(tempZipPath); + + // 清理临时文件 + if (File.Exists(tempZipPath)) + { + File.Delete(tempZipPath); + } + + Toast.Success("在线脚本仓库下载并导入成功!"); + } + catch (Exception ex) + { + Toast.Error($"下载失败: {ex.Message}"); + } + finally + { + IsUpdating = false; + } + }*/ + + [RelayCommand] + private async Task ImportLocalScriptsRepoZip() + { + if (IsUpdating) + { + Toast.Warning("请等待当前操作完成后再进行导入。"); + return; + } + + try + { + var openFileDialog = new OpenFileDialog + { + Title = "选择脚本仓库压缩包", + Filter = "压缩包文件 (*.zip)|*.zip", + Multiselect = false + }; + + if (openFileDialog.ShowDialog() == true) + { + IsUpdating = true; + UpdateProgressValue = 0; + UpdateProgressText = "正在导入脚本仓库,请耐心等待..."; + + await ImportZipFile(openFileDialog.FileName); + Toast.Success("脚本仓库导入成功!"); + } + } + catch (Exception ex) + { + Toast.Error($"导入失败: {ex.Message}"); + } + finally + { + IsUpdating = false; + } + } + + private async Task ImportZipFile(string zipFilePath) + { + await Task.Run(() => + { + var tempPath = ScriptRepoUpdater.ReposTempPath; + try + { + // 阶段1: 准备工作 (0-10%) + Dispatcher.Invoke(() => + { + UpdateProgressValue = 0; + UpdateProgressText = "正在准备导入环境..."; + }); + + var tempUnzipDir = Path.Combine(tempPath, "importZipFile"); + + // 先删除临时目录 + DirectoryHelper.DeleteReadOnlyDirectory(tempPath); + + // 创建目标目录 + Directory.CreateDirectory(tempUnzipDir); + + Dispatcher.Invoke(() => + { + UpdateProgressValue = 10; + UpdateProgressText = "准备完成,开始解压文件..."; + }); + + // 阶段2: 解压zip文件 (10-50%) + ZipFile.ExtractToDirectory(zipFilePath, tempUnzipDir, true); + + Dispatcher.Invoke(() => + { + UpdateProgressValue = 50; + UpdateProgressText = "文件解压完成,正在验证仓库结构..."; + }); + + // 阶段3: 查找并验证 repo.json (50-60%) + var repoJsonPath = Directory.GetFiles(tempUnzipDir, "repo.json", SearchOption.AllDirectories).FirstOrDefault(); + if (repoJsonPath == null) + { + throw new FileNotFoundException("未找到 repo.json 文件,导入失败。"); + } + + var repoDir = Path.GetDirectoryName(repoJsonPath)!; + + Dispatcher.Invoke(() => + { + UpdateProgressValue = 60; + UpdateProgressText = "仓库结构验证通过,正在清理旧数据..."; + }); + + // 阶段4: 删除旧的目标目录 (60-70%) + if (Directory.Exists(ScriptRepoUpdater.CenterRepoPath)) + { + DirectoryHelper.DeleteReadOnlyDirectory(ScriptRepoUpdater.CenterRepoPath); + } + + Dispatcher.Invoke(() => + { + UpdateProgressValue = 70; + UpdateProgressText = "旧数据清理完成,正在复制新仓库..."; + }); + + // 阶段5: 复制新目录 (70-95%) + DirectoryHelper.CopyDirectory(repoDir, ScriptRepoUpdater.CenterRepoPath); + + Dispatcher.Invoke(() => + { + UpdateProgressValue = 95; + UpdateProgressText = "仓库复制完成,正在清理临时文件..."; + }); + } + finally + { + // 阶段6: 清理临时文件 (95-100%) + DirectoryHelper.DeleteReadOnlyDirectory(tempPath); + } + + }); + + // 最终完成 + Dispatcher.Invoke(() => + { + UpdateProgressValue = 100; + UpdateProgressText = "导入完成"; + }); + } + + /// + /// 处理超链接点击事件 + /// + /// + /// + private void Hyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e) + { + try + { + Process.Start(new ProcessStartInfo + { + FileName = e.Uri.AbsoluteUri, + UseShellExecute = true + }); + } + catch (Exception ex) + { + MessageBox.Show($"无法打开链接: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Warning); + } + e.Handled = true; + } } \ No newline at end of file diff --git a/BetterGenshinImpact/ViewModel/MainWindowViewModel.cs b/BetterGenshinImpact/ViewModel/MainWindowViewModel.cs index 113d9492..d137cbf2 100644 --- a/BetterGenshinImpact/ViewModel/MainWindowViewModel.cs +++ b/BetterGenshinImpact/ViewModel/MainWindowViewModel.cs @@ -131,7 +131,30 @@ public partial class MainWindowViewModel : ObservableObject, IViewModel private void ApplyTheme(ThemeType themeType) { + var originalThemeType = themeType; + // 根据主题类型设置应用程序主题(深色/浅色)和背景效果类型(Mica/Acrylic/None) + if (!OsVersionHelper.IsWindows11_22523_OrGreater) + { + // 22523以下版本只支持深浅色切换,修正背景材质为纯色 + if (themeType == ThemeType.DarkMica || themeType == ThemeType.DarkAcrylic) + { + themeType = ThemeType.DarkNone; + } + else if (themeType == ThemeType.LightMica || themeType == ThemeType.LightAcrylic) + { + themeType = ThemeType.LightNone; + } + } + + // 如果主题类型被修正,更新配置并保存 + if (themeType != originalThemeType) + { + Config.CommonConfig.CurrentThemeType = themeType; + _configService.Save(); + _logger.LogInformation($"主题类型已从 {originalThemeType} 修正为 {themeType},因为当前系统不支持该主题效果"); + } + switch (themeType) { case ThemeType.DarkNone: diff --git a/BetterGenshinImpact/ViewModel/Pages/HotKeyPageViewModel.cs b/BetterGenshinImpact/ViewModel/Pages/HotKeyPageViewModel.cs index 3d0c12a4..7ed038bd 100644 --- a/BetterGenshinImpact/ViewModel/Pages/HotKeyPageViewModel.cs +++ b/BetterGenshinImpact/ViewModel/Pages/HotKeyPageViewModel.cs @@ -1,43 +1,44 @@ -using BetterGenshinImpact.Core.Config; +using BetterGenshinImpact.Core.Config; +using BetterGenshinImpact.Core.Recognition.OCR; +using BetterGenshinImpact.Core.Recognition.OpenCv; using BetterGenshinImpact.Core.Recorder; using BetterGenshinImpact.Core.Script; using BetterGenshinImpact.Core.Simulator; using BetterGenshinImpact.GameTask; +using BetterGenshinImpact.GameTask.AutoArtifactSalvage; using BetterGenshinImpact.GameTask.AutoFight; +using BetterGenshinImpact.GameTask.AutoFight.Assets; using BetterGenshinImpact.GameTask.AutoPathing; using BetterGenshinImpact.GameTask.AutoPathing.Handler; using BetterGenshinImpact.GameTask.AutoTrackPath; using BetterGenshinImpact.GameTask.Common; using BetterGenshinImpact.GameTask.Common.BgiVision; using BetterGenshinImpact.GameTask.Common.Job; +using BetterGenshinImpact.GameTask.Common.Map.Maps.Base; using BetterGenshinImpact.GameTask.Macro; +using BetterGenshinImpact.GameTask.Model.Area; +using BetterGenshinImpact.GameTask.QuickBuy; using BetterGenshinImpact.GameTask.QuickSereniteaPot; +using BetterGenshinImpact.GameTask.QuickTeleport.Assets; +using BetterGenshinImpact.GameTask.UseRedeemCode; using BetterGenshinImpact.Helpers; using BetterGenshinImpact.Helpers.Extensions; using BetterGenshinImpact.Model; using BetterGenshinImpact.Service.Interface; +using BetterGenshinImpact.View; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging.Messages; using Microsoft.Extensions.Logging; +using OpenCvSharp; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; +using System.Globalization; using System.Reflection; using System.Threading; using System.Threading.Tasks; -using BetterGenshinImpact.Core.Recognition.OCR; -using BetterGenshinImpact.Core.Recognition.OpenCv; -using BetterGenshinImpact.GameTask.AutoArtifactSalvage; -using BetterGenshinImpact.GameTask.AutoFight.Assets; -using BetterGenshinImpact.GameTask.Common.Map.Maps.Base; -using BetterGenshinImpact.GameTask.Model.Area; -using BetterGenshinImpact.GameTask.QuickBuy; -using BetterGenshinImpact.GameTask.QuickTeleport.Assets; -using BetterGenshinImpact.GameTask.UseRedeemCode; -using BetterGenshinImpact.View; -using OpenCvSharp; using Vanara.PInvoke; using HotKeySettingModel = BetterGenshinImpact.Model.HotKeySettingModel; @@ -533,7 +534,7 @@ public partial class HotKeyPageViewModel : ObservableObject, IViewModel if (pathRecording) { Task.Run(() => { pathRecorder.AddWaypoint(); }); - + } } )); @@ -583,7 +584,7 @@ public partial class HotKeyPageViewModel : ObservableObject, IViewModel Config.HotKeyConfig.Test1HotkeyType, (_, _) => { - Task.Run(async () => { await new AutoArtifactSalvageTask(4).Start(new CancellationToken()); }); + Task.Run(async () => { await new AutoArtifactSalvageTask(new AutoArtifactSalvageTaskParam(star: 4, null, null, null, null)).Start(new CancellationToken()); }); } )); diff --git a/BetterGenshinImpact/ViewModel/Pages/ScriptControlViewModel.cs b/BetterGenshinImpact/ViewModel/Pages/ScriptControlViewModel.cs index a9933d51..09d696ca 100644 --- a/BetterGenshinImpact/ViewModel/Pages/ScriptControlViewModel.cs +++ b/BetterGenshinImpact/ViewModel/Pages/ScriptControlViewModel.cs @@ -51,7 +51,7 @@ public partial class ScriptControlViewModel : ViewModel private readonly ILogger _logger = App.GetLogger(); private readonly IScriptService _scriptService; - + /// /// 配置组配置 /// @@ -143,13 +143,13 @@ public partial class ScriptControlViewModel : ViewModel GameInfo? gameInfo = null; var config = LogParse.LoadConfig(); - + OtherConfig.Miyoushe mcfg = TaskContext.Instance().Config.OtherConfig.MiyousheConfig; if (mcfg.LogSyncCookie && !string.IsNullOrEmpty(mcfg.Cookie)) { config.Cookie = mcfg.Cookie; } - + if (!string.IsNullOrEmpty(config.Cookie)) { config.CookieDictionary.TryGetValue(config.Cookie, out gameInfo); @@ -220,7 +220,7 @@ public partial class ScriptControlViewModel : ViewModel VerticalAlignment = VerticalAlignment.Center }; stackPanel.Children.Add(mergerStatsSwitch); - + // 开关控件:ToggleButton 或 CheckBox CheckBox faultStatsSwitch = new CheckBox { @@ -228,21 +228,21 @@ public partial class ScriptControlViewModel : ViewModel VerticalAlignment = VerticalAlignment.Center }; stackPanel.Children.Add(faultStatsSwitch); - + // 开关控件:ToggleButton 或 CheckBox CheckBox hoeingStatsSwitch = new CheckBox { Content = "统计锄地摩拉怪物数", VerticalAlignment = VerticalAlignment.Center }; - + CheckBox GenerateFarmingPlanData = new CheckBox { Content = "生成锄地规划数据", VerticalAlignment = VerticalAlignment.Center }; stackPanel.Children.Add(GenerateFarmingPlanData); - + //firstRow.Children.Add(toggleSwitch); // 将第一行添加到 StackPanel @@ -338,7 +338,7 @@ public partial class ScriptControlViewModel : ViewModel GenerateFarmingPlanData.IsChecked = sgpc.GenerateFarmingPlanData; faultStatsSwitch.IsChecked = sgpc.FaultStatsSwitch; mergerStatsSwitch.IsChecked = sgpc.MergerStatsSwitch; - + hoeingDelayTextBox.Text = sgpc.HoeingDelay; MessageBoxResult result = await uiMessageBox.ShowDialogAsync(); @@ -364,9 +364,9 @@ public partial class ScriptControlViewModel : ViewModel if (mcfg.LogSyncCookie && !string.IsNullOrEmpty(cookieValue)) { - mcfg.Cookie = cookieValue; + mcfg.Cookie = cookieValue; } - + LogParse.WriteConfigFile(config); @@ -383,7 +383,7 @@ public partial class ScriptControlViewModel : ViewModel { Application.Current.Dispatcher.Invoke(() => { - Toast.Information(status, time:5000); + Toast.Information(status, time: 5000); }); } @@ -465,7 +465,7 @@ public partial class ScriptControlViewModel : ViewModel // 生成HTML并加载 win.NavigateToHtml(LogParse.GenerHtmlByConfigGroupEntity(configGroupEntities, hoeingStats ? realGameInfo : null, sgpc)); - win.ShowDialog(); + win.ShowDialog(); // 取消订阅事件 LogParse.HtmlGenerationStatusChanged -= OnHtmlGenerationStatusChanged; @@ -559,14 +559,14 @@ public partial class ScriptControlViewModel : ViewModel private void ExportMergerJsons() { int count = 0; - var pathDir = Path.Combine(LogPath,"exportMergerJson",DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(),"AutoPathing"); + var pathDir = Path.Combine(LogPath, "exportMergerJson", DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), "AutoPathing"); foreach (var scriptGroupProject in SelectedScriptGroup?.Projects ?? []) { if (scriptGroupProject.Type == "Pathing") { - var mergerJson= JsonMerger.getMergePathingJson(Path.Combine(MapPathingViewModel.PathJsonPath, + var mergerJson = JsonMerger.getMergePathingJson(Path.Combine(MapPathingViewModel.PathJsonPath, scriptGroupProject.FolderName, scriptGroupProject.Name)); - string fullPath = Path.Combine(pathDir,scriptGroupProject.FolderName,scriptGroupProject.Name); + string fullPath = Path.Combine(pathDir, scriptGroupProject.FolderName, scriptGroupProject.Name); string dir = Path.GetDirectoryName(fullPath); if (!Directory.Exists(dir)) { @@ -576,13 +576,13 @@ public partial class ScriptControlViewModel : ViewModel count++; } } - if (count>0) + if (count > 0) { Process.Start("explorer.exe", pathDir); } } - - + + [RelayCommand] public void AddScriptGroupNextFlag(ScriptGroup? item) { @@ -591,7 +591,7 @@ public partial class ScriptControlViewModel : ViewModel scriptGroup.NextFlag = false; } - if (item!=null) + if (item != null) { item.NextFlag = true; TaskContext.Instance().Config.NextScriptGroupName = item.Name; @@ -731,7 +731,7 @@ public partial class ScriptControlViewModel : ViewModel private void OnAddJsScript() { var list = LoadAllJsScriptProjects(); - var stackPanel = CreateJsScriptSelectionPanel(list); + var stackPanel = CreateJsScriptSelectionPanel(list, typeof(CheckBox)); var result = PromptDialog.Prompt("请选择需要添加的JS脚本", "请选择需要添加的JS脚本", stackPanel, new Size(500, 600)); if (!string.IsNullOrEmpty(result)) @@ -740,19 +740,19 @@ public partial class ScriptControlViewModel : ViewModel } } - private ScrollViewer CreateJsScriptSelectionPanel(List list) + internal static ScrollViewer CreateJsScriptSelectionPanel(List list, Type selectType) { var stackPanel = new StackPanel(); - + var filterTextBox = new TextBox { Margin = new Thickness(0, 0, 0, 10), PlaceholderText = "输入搜索条件...", }; - filterTextBox.TextChanged += delegate { ApplyJsScriptFilter(stackPanel, list, filterTextBox.Text); }; + filterTextBox.TextChanged += delegate { ApplyJsScriptFilter(stackPanel, list, filterTextBox.Text, selectType); }; stackPanel.Children.Add(filterTextBox); - - AddJsScriptsToPanel(stackPanel, list, filterTextBox.Text); + + AddJsScriptsToPanel(stackPanel, list, filterTextBox.Text, selectType); var scrollViewer = new ScrollViewer { @@ -764,7 +764,7 @@ public partial class ScriptControlViewModel : ViewModel return scrollViewer; } - private void ApplyJsScriptFilter(StackPanel parentPanel, List scripts, string filter) + private static void ApplyJsScriptFilter(StackPanel parentPanel, List scripts, string filter, Type selectType) { if (parentPanel.Children.Count > 0) { @@ -780,16 +780,16 @@ public partial class ScriptControlViewModel : ViewModel removeElements.ForEach(parentPanel.Children.Remove); } - AddJsScriptsToPanel(parentPanel, scripts, filter); + AddJsScriptsToPanel(parentPanel, scripts, filter, selectType); } - private void AddJsScriptsToPanel(StackPanel parentPanel, List scripts, string filter) + private static void AddJsScriptsToPanel(StackPanel parentPanel, List scripts, string filter, Type selectType) { foreach (var script in scripts) { var displayText = script.FolderName + " - " + script.Manifest.Name; - - if (!string.IsNullOrEmpty(filter) && + + if (!string.IsNullOrEmpty(filter) && !displayText.Contains(filter, StringComparison.OrdinalIgnoreCase) && !script.FolderName.Contains(filter, StringComparison.OrdinalIgnoreCase) && !script.Manifest.Name.Contains(filter, StringComparison.OrdinalIgnoreCase)) @@ -797,15 +797,33 @@ public partial class ScriptControlViewModel : ViewModel continue; } - var checkBox = new CheckBox + if (selectType == typeof(CheckBox)) { - Content = displayText, - Tag = script.FolderName, - Margin = new Thickness(0, 2, 0, 2), - Name = "dynamic_" + Guid.NewGuid().ToString().Replace("-", "_") - }; - - parentPanel.Children.Add(checkBox); + var checkBox = new CheckBox + { + Content = displayText, + Tag = script.FolderName, + Margin = new Thickness(0, 2, 0, 2), + Name = "dynamic_" + Guid.NewGuid().ToString().Replace("-", "_") + }; + parentPanel.Children.Add(checkBox); + } + else if (selectType == typeof(RadioButton)) + { + var radioButton = new RadioButton + { + Content = displayText, + Tag = script.FolderName, + Margin = new Thickness(0, 2, 0, 2), + Name = "dynamic_" + Guid.NewGuid().ToString().Replace("-", "_"), + GroupName = "JsScriptsRadioButtonGroup" + }; + parentPanel.Children.Add(radioButton); + } + else + { + throw new ArgumentOutOfRangeException(); + } } } @@ -844,7 +862,7 @@ public partial class ScriptControlViewModel : ViewModel [RelayCommand] private void OnAddShell() { - var str = PromptDialog.Prompt("执行 shell 操作存在极大风险!请勿输入你看不懂的指令!以免引发安全隐患并损坏系统!\n执行 shell 的时候,游戏可能会失去焦点","请输入需要执行的shell"); + var str = PromptDialog.Prompt("执行 shell 操作存在极大风险!请勿输入你看不懂的指令!以免引发安全隐患并损坏系统!\n执行 shell 的时候,游戏可能会失去焦点", "请输入需要执行的shell"); if (!string.IsNullOrEmpty(str)) { SelectedScriptGroup?.AddProject(ScriptGroupProject.BuildShellProject(str)); @@ -1067,7 +1085,7 @@ public partial class ScriptControlViewModel : ViewModel // 递归时,传递当前节点的匹配状态 // 每个子节点深度相同,所以如果递归过程中任意子节点应该显示,则当前节点也应该显示 if (ShouldShowNode(child, filter, isDeepSearch, currentDepth + 1, currentNodeMatches)) - return true; + return true; } } @@ -1248,7 +1266,7 @@ public partial class ScriptControlViewModel : ViewModel // return result; // } - private List LoadAllJsScriptProjects() + internal static List LoadAllJsScriptProjects() { var path = Global.ScriptPath(); Directory.CreateDirectory(path); @@ -1394,13 +1412,13 @@ public partial class ScriptControlViewModel : ViewModel } [RelayCommand] - public async void OnDeleteScriptByFolder(ScriptGroupProject? item) + public async void OnDeleteScriptByFolder(ScriptGroupProject? item) { if (item == null) { return; - } - + } + if (SelectedScriptGroup != null) { var toBeDeletedProjects = SelectedScriptGroup.Projects @@ -1411,7 +1429,7 @@ public partial class ScriptControlViewModel : ViewModel { SelectedScriptGroup.Projects.Remove(project); } - + _snackbarService.Show( "脚本配置移除成功", $"已移除 {item.FolderName} 下的所有关联配置", @@ -1657,13 +1675,13 @@ public partial class ScriptControlViewModel : ViewModel RunnerContext.Instance.Reset(); TaskProgress taskProgress = new() - { - ScriptGroupNames = [SelectedScriptGroup.Name] - }; + { + ScriptGroupNames = [SelectedScriptGroup.Name] + }; RunnerContext.Instance.taskProgress = taskProgress; taskProgress.CurrentScriptGroupName = SelectedScriptGroup.Name; TaskProgressManager.SaveTaskProgress(taskProgress); - await _scriptService.RunMulti(GetNextProjects(SelectedScriptGroup), SelectedScriptGroup.Name,taskProgress); + await _scriptService.RunMulti(GetNextProjects(SelectedScriptGroup), SelectedScriptGroup.Name, taskProgress); } [RelayCommand] @@ -1737,7 +1755,7 @@ public partial class ScriptControlViewModel : ViewModel { SetTaskContextNextFlag(group); List ls = new List(); - if (group.Projects.Where(g=>g.NextFlag ?? false).Count() > 0) + if (group.Projects.Where(g => g.NextFlag ?? false).Count() > 0) { bool start = false; foreach (var item in group.Projects) @@ -1780,17 +1798,17 @@ public partial class ScriptControlViewModel : ViewModel return ls; } - - return group.Projects.Select(g=>g).ToList(); + + return group.Projects.Select(g => g).ToList(); } [RelayCommand] public async Task OnContinueMultiScriptGroupAsync() { - // 创建一个 StackPanel 来包含全选按钮和所有配置组的 CheckBox + // 创建一个 StackPanel 来包含全选按钮和所有配置组的 CheckBox var stackPanel = new StackPanel(); - + // 添加分割线 var separator = new Separator @@ -1800,21 +1818,21 @@ public partial class ScriptControlViewModel : ViewModel stackPanel.Children.Add(separator); List taskProgresses = TaskProgressManager.LoadAllTaskProgress(); - var checkBox = new ComboBox();; + var checkBox = new ComboBox(); ; stackPanel.Children.Add(checkBox); - ObservableCollection> kvs=new ObservableCollection>(); + ObservableCollection> kvs = new ObservableCollection>(); foreach (var taskProgress in taskProgresses) { - var name = taskProgress.Name+"_"+taskProgress.CurrentScriptGroupName+"_"; + var name = taskProgress.Name + "_" + taskProgress.CurrentScriptGroupName + "_"; if (taskProgress.Loop) { - name += "循环("+taskProgress.LoopCount+")_"; + name += "循环(" + taskProgress.LoopCount + ")_"; } - if (taskProgress.CurrentScriptGroupProjectInfo!=null) + if (taskProgress.CurrentScriptGroupProjectInfo != null) { - name = name +taskProgress.CurrentScriptGroupProjectInfo.Index+ "_" + taskProgress.CurrentScriptGroupProjectInfo.Name; + name = name + taskProgress.CurrentScriptGroupProjectInfo.Index + "_" + taskProgress.CurrentScriptGroupProjectInfo.Name; } - kvs.Add(new KeyValuePair(taskProgress.Name,name)); + kvs.Add(new KeyValuePair(taskProgress.Name, name)); } checkBox.SelectedValuePath = "Key"; @@ -1822,7 +1840,7 @@ public partial class ScriptControlViewModel : ViewModel checkBox.ItemsSource = kvs; checkBox.SelectedIndex = 0; //SelectedValuePath="Key" - // DisplayMemberPath="Value" + // DisplayMemberPath="Value" var uiMessageBox = new Wpf.Ui.Controls.MessageBox { Title = "选择需要继续执行的进度记录", @@ -1831,7 +1849,8 @@ public partial class ScriptControlViewModel : ViewModel Content = stackPanel, VerticalScrollBarVisibility = ScrollBarVisibility.Auto, Height = 300 // 设置固定高度 - ,Width = 600 + , + Width = 600 }, CloseButtonText = "关闭", PrimaryButtonText = "确认执行", @@ -1842,7 +1861,7 @@ public partial class ScriptControlViewModel : ViewModel var result = await uiMessageBox.ShowDialogAsync(); if (result == MessageBoxResult.Primary) { - + /*var selectedGroups = checkBoxes .Where(kv => kv.Value.IsChecked == true) .Select(kv => kv.Key) @@ -1857,7 +1876,7 @@ public partial class ScriptControlViewModel : ViewModel } } - public async Task OnContinueTaskProgressAsync(string name,List? taskProgresses = null) + public async Task OnContinueTaskProgressAsync(string name, List? taskProgresses = null) { if (taskProgresses == null) { @@ -1873,24 +1892,24 @@ public partial class ScriptControlViewModel : ViewModel } else { - taskProgress=taskProgresses.FirstOrDefault(t=>t.Name == name); + taskProgress = taskProgresses.FirstOrDefault(t => t.Name == name); } - - - if (taskProgress!=null) + + + if (taskProgress != null) { //await StartGroups(selectedGroups); //taskProgress.Next var sg = ScriptGroups.ToList().Where(sg => taskProgress.ScriptGroupNames.Contains(sg.Name)).ToList(); - TaskProgressManager.GenerNextProjectInfo(taskProgress,sg); - if (taskProgress.Next==null) + TaskProgressManager.GenerNextProjectInfo(taskProgress, sg); + if (taskProgress.Next == null) { - _logger.LogWarning("无法定位到下一个要执行的项目:next为空("+taskProgress.Name+")"); + _logger.LogWarning("无法定位到下一个要执行的项目:next为空(" + taskProgress.Name + ")"); } else { - await StartGroups(sg,taskProgress); + await StartGroups(sg, taskProgress); } } @@ -1927,13 +1946,13 @@ public partial class ScriptControlViewModel : ViewModel var stackPanel = new StackPanel(); var checkBoxes = new Dictionary(); - + var loopCheckBox = new CheckBox { Content = "循环", }; - - + + // 创建全选按钮 var selectAllCheckBox = new CheckBox { @@ -2046,7 +2065,7 @@ public partial class ScriptControlViewModel : ViewModel ); return; } - await StartGroups(selectedGroups,null,loopCheckBox.IsChecked ?? false);; + await StartGroups(selectedGroups, null, loopCheckBox.IsChecked ?? false); } } @@ -2057,7 +2076,7 @@ public partial class ScriptControlViewModel : ViewModel public async Task OnStartMultiScriptGroupWithNamesAsync(params string[] names) { - if( ScriptGroups.Count == 0) + if (ScriptGroups.Count == 0) { ReadScriptGroup(); } @@ -2085,7 +2104,7 @@ public partial class ScriptControlViewModel : ViewModel } } - public async Task StartGroups(List scriptGroups,TaskProgress? taskProgress = null,bool loop = false) + public async Task StartGroups(List scriptGroups, TaskProgress? taskProgress = null, bool loop = false) { _logger.LogInformation("开始连续执行选中配置组:{Names}", string.Join(",", scriptGroups.Select(x => x.Name))); try @@ -2096,7 +2115,8 @@ public partial class ScriptControlViewModel : ViewModel taskProgress = new() { ScriptGroupNames = scriptGroups.Select(x => x.Name).ToList() - ,Loop = loop + , + Loop = loop }; } @@ -2104,16 +2124,16 @@ public partial class ScriptControlViewModel : ViewModel var sg = GetNextScriptGroups(scriptGroups); foreach (var scriptGroup in sg) { - if (taskProgress.Next!=null) + if (taskProgress.Next != null) { - if (scriptGroup.Name!=taskProgress.Next.GroupName) + if (scriptGroup.Name != taskProgress.Next.GroupName) { continue; } } taskProgress.CurrentScriptGroupName = scriptGroup.Name; TaskProgressManager.SaveTaskProgress(taskProgress); - await _scriptService.RunMulti(GetNextProjects(scriptGroup), scriptGroup.Name,taskProgress); + await _scriptService.RunMulti(GetNextProjects(scriptGroup), scriptGroup.Name, taskProgress); await Task.Delay(2000); } @@ -2133,7 +2153,7 @@ public partial class ScriptControlViewModel : ViewModel taskProgress.EndTime = DateTime.Now; TaskProgressManager.SaveTaskProgress(taskProgress); } - + } } catch (Exception e) diff --git a/BetterGenshinImpact/ViewModel/Pages/TaskSettingsPageViewModel.cs b/BetterGenshinImpact/ViewModel/Pages/TaskSettingsPageViewModel.cs index 19e1901c..96bd4c4f 100644 --- a/BetterGenshinImpact/ViewModel/Pages/TaskSettingsPageViewModel.cs +++ b/BetterGenshinImpact/ViewModel/Pages/TaskSettingsPageViewModel.cs @@ -1,44 +1,41 @@ using BetterGenshinImpact.Core.Config; using BetterGenshinImpact.Core.Script; +using BetterGenshinImpact.Core.Script.Project; using BetterGenshinImpact.GameTask; +using BetterGenshinImpact.GameTask.AutoArtifactSalvage; using BetterGenshinImpact.GameTask.AutoDomain; using BetterGenshinImpact.GameTask.AutoFight; +using BetterGenshinImpact.GameTask.AutoFishing; using BetterGenshinImpact.GameTask.AutoGeniusInvokation; using BetterGenshinImpact.GameTask.AutoMusicGame; -using BetterGenshinImpact.GameTask.AutoSkip.Model; -using BetterGenshinImpact.GameTask.AutoTrackPath; -using BetterGenshinImpact.GameTask.AutoWood; -using BetterGenshinImpact.GameTask.Model; -using BetterGenshinImpact.Service.Interface; -using BetterGenshinImpact.View.Pages; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Windows.System; -using BetterGenshinImpact.GameTask.AutoFishing; -using BetterGenshinImpact.GameTask.Common.Element.Assets; - -using BetterGenshinImpact.Helpers; -using Wpf.Ui; -using Wpf.Ui.Violeta.Controls; -using BetterGenshinImpact.ViewModel.Pages.View; -using System.Linq; -using System.Reflection; -using System.Collections.Frozen; -using System.Diagnostics; -using System.Windows; -using System.Windows.Controls; -using BetterGenshinImpact.GameTask.AutoArtifactSalvage; using BetterGenshinImpact.GameTask.AutoStygianOnslaught; -using BetterGenshinImpact.View.Windows; +using BetterGenshinImpact.GameTask.AutoWood; +using BetterGenshinImpact.GameTask.Common.Element.Assets; using BetterGenshinImpact.GameTask.GetGridIcons; using BetterGenshinImpact.GameTask.Model.GameUI; using BetterGenshinImpact.GameTask.UseRedeemCode; +using BetterGenshinImpact.Helpers; +using BetterGenshinImpact.Service.Interface; +using BetterGenshinImpact.View.Pages; +using BetterGenshinImpact.View.Windows; +using BetterGenshinImpact.ViewModel.Pages.View; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using Windows.System; +using Wpf.Ui; +using Wpf.Ui.Violeta.Controls; using TextBox = Wpf.Ui.Controls.TextBox; namespace BetterGenshinImpact.ViewModel.Pages; @@ -162,6 +159,16 @@ public partial class TaskSettingsPageViewModel : ViewModel [ObservableProperty] private bool _switchArtifactSalvageEnabled; + [ObservableProperty] + private FrozenDictionary _recognitionFailurePolicyDict = Enum.GetValues(typeof(RecognitionFailurePolicy)) + .Cast() + .ToFrozenDictionary( + e => (Enum)e, + e => e.GetType() + .GetField(e.ToString())? + .GetCustomAttribute()? + .Description ?? e.ToString()); + [ObservableProperty] private bool _switchGetGridIconsEnabled; [ObservableProperty] @@ -527,7 +534,13 @@ public partial class TaskSettingsPageViewModel : ViewModel { SwitchArtifactSalvageEnabled = true; await new TaskRunner() - .RunSoloTaskAsync(new AutoArtifactSalvageTask(int.Parse(Config.AutoArtifactSalvageConfig.MaxArtifactStar), Config.AutoArtifactSalvageConfig.JavaScript, Config.AutoArtifactSalvageConfig.MaxNumToCheck)); + .RunSoloTaskAsync(new AutoArtifactSalvageTask(new AutoArtifactSalvageTaskParam( + int.Parse(Config.AutoArtifactSalvageConfig.MaxArtifactStar), + Config.AutoArtifactSalvageConfig.JavaScript, + Config.AutoArtifactSalvageConfig.ArtifactSetFilter, + Config.AutoArtifactSalvageConfig.MaxNumToCheck, + Config.AutoArtifactSalvageConfig.RecognitionFailurePolicy + ))); SwitchArtifactSalvageEnabled = false; } @@ -540,10 +553,56 @@ public partial class TaskSettingsPageViewModel : ViewModel [RelayCommand] private void OnOpenArtifactSalvageTestOCRWindow() { - OcrDialog ocrDialog = new OcrDialog(0.70, 0.098, 0.24, 0.52, "圣遗物分解", this.Config.AutoArtifactSalvageConfig.JavaScript); + OcrDialog ocrDialog = new OcrDialog(0.70, 0.112, 0.275, 0.50, "圣遗物分解", this.Config.AutoArtifactSalvageConfig.JavaScript); ocrDialog.ShowDialog(); } + [RelayCommand] + private async Task OnCopyArtifactSalvageJavaScriptFromRepository() + { + var list = ScriptControlViewModel.LoadAllJsScriptProjects(); + var stackPanel = ScriptControlViewModel.CreateJsScriptSelectionPanel(list, typeof(RadioButton)); + + var result = PromptDialog.Prompt("请选择需要复制的JS脚本", "请选择需要复制的JS脚本", stackPanel, new Size(500, 600)); + if (!string.IsNullOrEmpty(result)) + { + string? selectedFolderName = null; + foreach (var child in ((Wpf.Ui.Controls.StackPanel)stackPanel.Content).Children) + { + if (child is RadioButton { IsChecked: true } radioButton && radioButton.Tag is string folderName) + { + selectedFolderName = folderName; + } + } + if (selectedFolderName == null) + { + return; + } + + ScriptProject scriptProject = new ScriptProject(selectedFolderName); + string jsCode = await scriptProject.LoadCode(); + + var multilineTextBox = new TextBox + { + TextWrapping = TextWrapping.Wrap, + VerticalScrollBarVisibility = ScrollBarVisibility.Auto, + Text = jsCode, + IsReadOnly = true + }; + var p = new PromptDialog($"{scriptProject.Manifest.Name}\r\n{scriptProject.Manifest.ShortDescription}\r\n\r\n将覆盖现有的JavaScript,是否继续?", $"预览 - {scriptProject.FolderName}", multilineTextBox, null); + p.Height = 600; + p.MaxWidth = 800; + p.ShowDialog(); + + if (p.DialogResult != true) + { + return; + } + + this.Config.AutoArtifactSalvageConfig.JavaScript = jsCode; + } + } + [RelayCommand] private async Task OnSwitchGetGridIcons() { diff --git a/BetterGenshinImpact/ViewModel/Pages/TriggerSettingsPageViewModel.cs b/BetterGenshinImpact/ViewModel/Pages/TriggerSettingsPageViewModel.cs index c9cd5048..d6a352f7 100644 --- a/BetterGenshinImpact/ViewModel/Pages/TriggerSettingsPageViewModel.cs +++ b/BetterGenshinImpact/ViewModel/Pages/TriggerSettingsPageViewModel.cs @@ -74,7 +74,11 @@ public partial class TriggerSettingsPageViewModel : ViewModel "牢固的箭簇" }; var p = new PromptDialog( - "黑名单配置,每行一条记录", + "黑名单配置,每行一条记录\n"+ + "示例:\n" + + "精致的宝箱\n" + + "史莱姆凝液\n" + + "牢固的箭簇", "黑名单配置", multilineTextBox, text); @@ -111,7 +115,11 @@ public partial class TriggerSettingsPageViewModel : ViewModel "启动" }; var p = new PromptDialog( - "白名单配置,每行一条记录", + "白名单配置,每行一条记录\n" + + "示例:\n" + + "调查\n" + + "合成\n" + + "启动", "白名单配置", multilineTextBox, text); diff --git a/BetterGenshinImpact/ViewModel/Pages/View/ScriptGroupConfigViewModel.cs b/BetterGenshinImpact/ViewModel/Pages/View/ScriptGroupConfigViewModel.cs index eb6e498c..3f4b6289 100644 --- a/BetterGenshinImpact/ViewModel/Pages/View/ScriptGroupConfigViewModel.cs +++ b/BetterGenshinImpact/ViewModel/Pages/View/ScriptGroupConfigViewModel.cs @@ -1,11 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; using BetterGenshinImpact.Core.Config; using BetterGenshinImpact.Core.Script.Group; -using BetterGenshinImpact.View.Windows; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using Windows.System; using Wpf.Ui.Violeta.Controls; namespace BetterGenshinImpact.ViewModel.Pages.View; @@ -96,4 +97,10 @@ public partial class ScriptGroupConfigViewModel : ObservableObject, IViewModel { PathingConfig.Enabled = true; } + + [RelayCommand] + private async Task OnGoToAutoEatUrlAsync() + { + await Launcher.LaunchUriAsync(new Uri("https://bettergi.com/dev/js/dispatcher.html#autoeat-自动吃食物")); + } } \ No newline at end of file diff --git a/Test/BetterGenshinImpact.UnitTest/CoreTests/RecognitionTests/OCRTests/PaddleFixture.cs b/Test/BetterGenshinImpact.UnitTest/CoreTests/RecognitionTests/OCRTests/PaddleFixture.cs index dd85b927..28f488ee 100644 --- a/Test/BetterGenshinImpact.UnitTest/CoreTests/RecognitionTests/OCRTests/PaddleFixture.cs +++ b/Test/BetterGenshinImpact.UnitTest/CoreTests/RecognitionTests/OCRTests/PaddleFixture.cs @@ -1,4 +1,4 @@ -using BetterGenshinImpact.Core.Recognition.ONNX; +using BetterGenshinImpact.Core.Recognition.ONNX; using System.Collections.Concurrent; using System.Globalization; using BetterGenshinImpact.Core.Recognition.OCR.Paddle; diff --git a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoArtifactSalvageTests/AutoArtifactSalvageTaskTests.cs b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoArtifactSalvageTests/AutoArtifactSalvageTaskTests.cs index 6aca25ca..bc450699 100644 --- a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoArtifactSalvageTests/AutoArtifactSalvageTaskTests.cs +++ b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoArtifactSalvageTests/AutoArtifactSalvageTaskTests.cs @@ -1,6 +1,7 @@ using BetterGenshinImpact.GameTask.AutoArtifactSalvage; using BetterGenshinImpact.GameTask.Model.GameUI; using BetterGenshinImpact.UnitTest.CoreTests.RecognitionTests.OCRTests; +using Microsoft.Extensions.Localization; using OpenCvSharp; using System; using System.Collections.Concurrent; @@ -13,12 +14,15 @@ using static BetterGenshinImpact.GameTask.AutoArtifactSalvage.AutoArtifactSalvag namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoArtifactSalvageTests { [Collection("Init Collection")] - public partial class AutoArtifactSalvageTaskTests + public class AutoArtifactSalvageTaskTests { private readonly PaddleFixture paddle; - public AutoArtifactSalvageTaskTests(PaddleFixture paddle) + private readonly IStringLocalizer stringLocalizer; + + public AutoArtifactSalvageTaskTests(PaddleFixture paddle, LocalizationFixture localization) { this.paddle = paddle; + this.stringLocalizer = localization.CreateStringLocalizer(); } [Theory] @@ -94,10 +98,11 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoArtifactSalvageTests CultureInfo cultureInfo = new CultureInfo("zh-Hans"); // + AutoArtifactSalvageTask sut = new AutoArtifactSalvageTask(new AutoArtifactSalvageTaskParam(5, null, null, null, null, cultureInfo, this.stringLocalizer), new FakeLogger()); string result = PaddleResultDic.GetOrAdd(screenshot, screenshot_ => { using Mat mat = new Mat(@$"..\..\..\Assets\AutoArtifactSalvage\{screenshot_}"); - GetArtifactStat(mat, paddle.Get(), cultureInfo, out string allText); + sut.GetArtifactStat(mat, paddle.Get(), out string allText); return allText; }); @@ -112,30 +117,105 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoArtifactSalvageTests } } + public static IEnumerable GetArtifactStatTestData + { + get + { + yield return new object[] { "ArtifactAffixes.png", new ArtifactStat("异种的期许", new ArtifactAffix(ArtifactAffixType.HP, 717f), [ + new ArtifactAffix(ArtifactAffixType.ElementalMastery, 16f), + new ArtifactAffix(ArtifactAffixType.EnergyRecharge, 6.5f), + new ArtifactAffix(ArtifactAffixType.ATKPercent, 5.8f), + new ArtifactAffix(ArtifactAffixType.DEF, 23f) + ], 0), new CultureInfo("zh-Hans") }; + yield return new object[] { "202508242224_GetArtifactStat.png", new ArtifactStat("裁判的时刻", new ArtifactAffix(ArtifactAffixType.DEFPercent, 8.7f), [ + new ArtifactAffix(ArtifactAffixType.ATK, 16f), + new ArtifactAffix(ArtifactAffixType.CRITDMG, 7.8f), + new ArtifactAffix(ArtifactAffixType.DEF, 19), + new ArtifactAffix(ArtifactAffixType.CRITRate, 2.7f) + ], 0), new CultureInfo("zh-Hans") }; + yield return new object[] { "202508252004_GetArtifactStat.png", new ArtifactStat("Deep Gallery's Bestowed Banquet", new ArtifactAffix(ArtifactAffixType.DEFPercent, 8.7f), [ + new ArtifactAffix(ArtifactAffixType.HP, 239), + new ArtifactAffix(ArtifactAffixType.ATK, 18), + new ArtifactAffix(ArtifactAffixType.HPPercent, 4.1f) + ], 0), new CultureInfo("en") }; + yield return new object[] { "20250827000123_GetArtifactStat.png", new ArtifactStat("Pacte distant des galeries profondes", new ArtifactAffix(ArtifactAffixType.ATK, 47), [ + new ArtifactAffix(ArtifactAffixType.DEFPercent, 7.3f), + new ArtifactAffix(ArtifactAffixType.DEF, 16), + new ArtifactAffix(ArtifactAffixType.HPPercent, 5.3f) + ], 0), new CultureInfo("fr") }; + yield return new object[] { "20250827000246_GetArtifactStat.png", new ArtifactStat("Reckoning of the Xenogenic", new ArtifactAffix(ArtifactAffixType.HP, 717f), [ + new ArtifactAffix(ArtifactAffixType.ElementalMastery, 16), + new ArtifactAffix(ArtifactAffixType.EnergyRecharge, 6.5f), + new ArtifactAffix(ArtifactAffixType.ATKPercent, 5.8f), + new ArtifactAffix(ArtifactAffixType.DEF, 23) + ], 0), new CultureInfo("en") }; + yield return new object[] { "20250827151043_GetArtifactStat.png", new ArtifactStat("Couronne de laurier", new ArtifactAffix(ArtifactAffixType.ATKPercent, 7.0f), [ + new ArtifactAffix(ArtifactAffixType.CRITDMG, 5.4f), + new ArtifactAffix(ArtifactAffixType.DEFPercent, 7.3f), + new ArtifactAffix(ArtifactAffixType.DEF, 16), + new ArtifactAffix(ArtifactAffixType.HPPercent, 5.8f) + ], 0), new CultureInfo("fr") }; // 一个百里挑一的识别失败的例子 + yield return new object[] { "20250827163340_GetArtifactStat.png", new ArtifactStat("Couronne de Watatsumi", new ArtifactAffix(ArtifactAffixType.DEFPercent, 8.7f), [ + new ArtifactAffix(ArtifactAffixType.HP, 299), + new ArtifactAffix(ArtifactAffixType.EnergyRecharge, 4.5f), + new ArtifactAffix(ArtifactAffixType.CRITDMG, 6.2f), + new ArtifactAffix(ArtifactAffixType.ElementalMastery, 23) + ], 0), new CultureInfo("fr") }; + yield return new object[] { "20250827204545_GetArtifactStat.png", new ArtifactStat("Agitation de la nuit dorée", new ArtifactAffix(ArtifactAffixType.GeoDMGBonus, 7.0f), [ + new ArtifactAffix(ArtifactAffixType.HP, 269), + new ArtifactAffix(ArtifactAffixType.ElementalMastery, 23), + new ArtifactAffix(ArtifactAffixType.CRITRate, 3.1f) + ], 0), new CultureInfo("fr") }; + yield return new object[] { "20250828084843_GetArtifactStat.png", new ArtifactStat("異種的期許", new ArtifactAffix(ArtifactAffixType.HP, 717f), [ + new ArtifactAffix(ArtifactAffixType.DEFPercent, 7.3f), + new ArtifactAffix(ArtifactAffixType.ElementalMastery, 23), + new ArtifactAffix(ArtifactAffixType.ATKPercent, 4.1f) + ], 0), new CultureInfo("zh-Hant") }; + yield return new object[] { "20250828093344_GetArtifactStat.png", new ArtifactStat("黃金時代的先聲",new ArtifactAffix(ArtifactAffixType.DEFPercent, 8.7f), [ + new ArtifactAffix(ArtifactAffixType.DEF, 19), + new ArtifactAffix(ArtifactAffixType.CRITDMG, 7.8f), + new ArtifactAffix(ArtifactAffixType.ATK, 18), + new ArtifactAffix(ArtifactAffixType.ElementalMastery, 23) + ], 0), new CultureInfo("zh-Hant") }; + } + } + /// /// 测试获取分解圣遗物界面右侧圣遗物的各种结构化信息,结果应正确 /// /// [Theory] - [InlineData(@"ArtifactAffixes.png")] - public void GetArtifactStat_AffixesShouldBeRight(string screenshot) + [MemberData(nameof(GetArtifactStatTestData))] + public void GetArtifactStat_AffixesShouldBeRight(string screenshot, ArtifactStat expectedArtifactStat, CultureInfo cultureInfo) { - // - CultureInfo cultureInfo = new CultureInfo("zh-Hans"); - // using Mat mat = new Mat(@$"..\..\..\Assets\AutoArtifactSalvage\{screenshot}"); - ArtifactStat artifact = GetArtifactStat(mat, paddle.Get(), cultureInfo, out string _); + + /* + using var gray = mat.CvtColor(ColorConversionCodes.BGR2GRAY); + using Mat kernel = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(15, 15)); + using var bottomHat = gray.MorphologyEx(MorphTypes.TopHat, kernel); + Cv2.ImShow("bottomHat", bottomHat); + Mat threshold = new Mat(); + var otsu = Cv2.Threshold(bottomHat, threshold, 30, 255, ThresholdTypes.Binary); + Cv2.ImShow($"thres = {otsu}", threshold); + Cv2.WaitKey(); + */ // - Assert.Equal("异种的期许", artifact.Name); - Assert.True(artifact.MainAffix.Type == ArtifactAffixType.HP); - Assert.True(artifact.MainAffix.Value == 717f); - Assert.Contains(artifact.MinorAffixes, a => a.Type == ArtifactAffixType.ElementalMastery && a.Value == 16f); - Assert.Contains(artifact.MinorAffixes, a => a.Type == ArtifactAffixType.EnergyRecharge && a.Value == 6.5f); - Assert.Contains(artifact.MinorAffixes, a => a.Type == ArtifactAffixType.ATKPercent && a.Value == 5.8f); - Assert.Contains(artifact.MinorAffixes, a => a.Type == ArtifactAffixType.DEF && a.Value == 23f); - Assert.True(artifact.Level == 0); + AutoArtifactSalvageTask sut = new AutoArtifactSalvageTask(new AutoArtifactSalvageTaskParam(5, null, null, null, null, cultureInfo, this.stringLocalizer), new FakeLogger()); + ArtifactStat result = sut.GetArtifactStat(mat, paddle.Get(cultureInfo.Name), out string _); + + // + Assert.Equal(expectedArtifactStat.Name, result.Name); + Assert.Equal(expectedArtifactStat.MainAffix.Type, result.MainAffix.Type); + Assert.Equal(expectedArtifactStat.MainAffix.Value, result.MainAffix.Value); + foreach (ArtifactAffix expectedArtifactAffix in expectedArtifactStat.MinorAffixes) + { + Assert.Contains(result.MinorAffixes, a => + a.Type == expectedArtifactAffix.Type && a.Value == expectedArtifactAffix.Value); + } + Assert.True(result.Level == expectedArtifactStat.Level); } [Theory] @@ -153,11 +233,12 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoArtifactSalvageTests public void IsMatchJavaScript_JSShouldBeRight(string screenshot, string js, bool expected) { // + using Mat mat = new Mat(@$"..\..\..\Assets\AutoArtifactSalvage\{screenshot}"); CultureInfo cultureInfo = new CultureInfo("zh-Hans"); // - using Mat mat = new Mat(@$"..\..\..\Assets\AutoArtifactSalvage\{screenshot}"); - ArtifactStat artifact = GetArtifactStat(mat, paddle.Get(), cultureInfo, out string _); + AutoArtifactSalvageTask sut = new AutoArtifactSalvageTask(new AutoArtifactSalvageTaskParam(5, null, null, null, null, cultureInfo, this.stringLocalizer), new FakeLogger()); + ArtifactStat artifact = sut.GetArtifactStat(mat, paddle.Get(), out string _); bool result = IsMatchJavaScript(artifact, js); // diff --git a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFishingTests/BehavioursTests.ChooseBait.cs b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFishingTests/BehavioursTests.ChooseBait.cs index cf70aaa8..d5cfa27d 100644 --- a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFishingTests/BehavioursTests.ChooseBait.cs +++ b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFishingTests/BehavioursTests.ChooseBait.cs @@ -1,4 +1,4 @@ -using BehaviourTree; +using BehaviourTree; using BetterGenshinImpact.GameTask.AutoFishing; using BetterGenshinImpact.GameTask.AutoFishing.Model; using BetterGenshinImpact.GameTask.Model.Area; @@ -17,7 +17,7 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests { [Theory] [InlineData(@"20250225101300361_ChooseBait_Succeeded.png", new string[] { "medaka", "butterflyfish", "butterflyfish", "pufferfish" })] - [InlineData(@"20250226161354285_ChooseBait_Succeeded.png", new string[] { "medaka", "medaka" })] + [InlineData(@"20250226161354285_ChooseBait_Succeeded.png", new string[] { "medaka", "medaka" })] // todo 更新用例 [InlineData(@"202503160917566615@900p.png", new string[] { "pufferfish" })] /// /// 测试各种选取鱼饵,结果为成功 @@ -35,7 +35,7 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests }; // - ChooseBait sut = new ChooseBait("-", blackboard, new FakeLogger(), false, systemInfo, new FakeInputSimulator()); + ChooseBait sut = new ChooseBait("-", blackboard, new FakeLogger(), false, systemInfo, new FakeInputSimulator(), this.session, this.prototypes); BehaviourStatus actual = sut.Tick(imageRegion); // @@ -69,11 +69,11 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests FakeTimeProvider fakeTimeProvider = new FakeTimeProvider(dateTime); // - ChooseBait sut = new ChooseBait("-", blackboard, new FakeLogger(), false, systemInfo, new FakeInputSimulator(), fakeTimeProvider); + ChooseBait sut = new ChooseBait("-", blackboard, new FakeLogger(), false, systemInfo, new FakeInputSimulator(), this.session, this.prototypes, fakeTimeProvider); BehaviourStatus actual = sut.Tick(imageRegion); // - Assert.True(String.IsNullOrEmpty(blackboard.selectedBaitName)); + Assert.Null(blackboard.selectedBait); Assert.True(blackboard.chooseBaitUIOpening); Assert.Equal(BehaviourStatus.Running, actual); @@ -84,7 +84,7 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests actual = sut.Tick(imageRegion); // - Assert.False(String.IsNullOrEmpty(blackboard.selectedBaitName)); + Assert.NotNull(blackboard.selectedBait); Assert.True(blackboard.chooseBaitUIOpening); Assert.Equal(BehaviourStatus.Running, actual); @@ -95,7 +95,7 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests actual = sut.Tick(imageRegion); // - Assert.True(String.IsNullOrEmpty(blackboard.selectedBaitName)); + Assert.Null(blackboard.selectedBait); Assert.False(blackboard.chooseBaitUIOpening); Assert.Equal(BehaviourStatus.Failed, actual); } @@ -124,15 +124,15 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests #region 第1次失败 // - ChooseBait sut = new ChooseBait("-", blackboard, new FakeLogger(), false, systemInfo, new FakeInputSimulator(), fakeTimeProvider); + ChooseBait sut = new ChooseBait("-", blackboard, new FakeLogger(), false, systemInfo, new FakeInputSimulator(), this.session, this.prototypes, fakeTimeProvider); BehaviourStatus actual = sut.Tick(imageRegion); fakeTimeProvider.SetUtcNow(dateTime.AddSeconds(3)); actual = sut.Tick(imageRegion); // - Assert.True(String.IsNullOrEmpty(blackboard.selectedBaitName)); + Assert.Null(blackboard.selectedBait); Assert.Equal(BehaviourStatus.Failed, actual); - Assert.Single(blackboard.chooseBaitFailures.Where(f => f == "fake fly bait")); + Assert.Single(blackboard.chooseBaitFailures.Where(f => f == BaitType.FakeFlyBait)); #endregion #region 第2次失败 @@ -146,9 +146,9 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests actual = sut.Tick(imageRegion); // - Assert.True(String.IsNullOrEmpty(blackboard.selectedBaitName)); + Assert.Null(blackboard.selectedBait); Assert.Equal(BehaviourStatus.Failed, actual); - Assert.Equal(2, blackboard.chooseBaitFailures.Where(f => f == "fake fly bait").Count()); + Assert.Equal(2, blackboard.chooseBaitFailures.Where(f => f == BaitType.FakeFlyBait).Count()); Assert.False(blackboard.abort); #endregion @@ -165,9 +165,9 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests actual = sut.Tick(imageRegion); // - Assert.True(String.IsNullOrEmpty(blackboard.selectedBaitName)); + Assert.Null(blackboard.selectedBait); Assert.Equal(BehaviourStatus.Failed, actual); - Assert.Single(blackboard.chooseBaitFailures.Where(f => f == "spinelgrain bait")); + Assert.Single(blackboard.chooseBaitFailures.Where(f => f == BaitType.SpinelgrainBait)); #endregion #region sunfish受到遮挡,medaka再次出现,第4次成功,并钓起medaka @@ -183,9 +183,9 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests actual = sut.Tick(imageRegion); // - Assert.False(String.IsNullOrEmpty(blackboard.selectedBaitName)); + Assert.NotNull(blackboard.selectedBait); // todo 更新用例 Assert.Equal(BehaviourStatus.Succeeded, actual); - Assert.Single(blackboard.chooseBaitFailures.Where(f => f == "spinelgrain bait")); + Assert.Single(blackboard.chooseBaitFailures.Where(f => f == BaitType.SpinelgrainBait)); #endregion #region sunfish再次出现,第5次失败 @@ -201,9 +201,9 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests actual = sut.Tick(imageRegion); // - Assert.True(String.IsNullOrEmpty(blackboard.selectedBaitName)); + Assert.Null(blackboard.selectedBait); Assert.Equal(BehaviourStatus.Failed, actual); - Assert.Equal(2, blackboard.chooseBaitFailures.Where(f => f == "spinelgrain bait").Count()); + Assert.Equal(2, blackboard.chooseBaitFailures.Where(f => f == BaitType.SpinelgrainBait).Count()); #endregion } @@ -230,15 +230,15 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests #region 第1次失败 // - ChooseBait sut = new ChooseBait("-", blackboard, new FakeLogger(), false, systemInfo, new FakeInputSimulator(), fakeTimeProvider); + ChooseBait sut = new ChooseBait("-", blackboard, new FakeLogger(), false, systemInfo, new FakeInputSimulator(), this.session, this.prototypes, fakeTimeProvider); BehaviourStatus actual = sut.Tick(imageRegion); fakeTimeProvider.SetUtcNow(dateTime.AddSeconds(3)); actual = sut.Tick(imageRegion); // - Assert.True(String.IsNullOrEmpty(blackboard.selectedBaitName)); + Assert.Null(blackboard.selectedBait); Assert.Equal(BehaviourStatus.Failed, actual); - Assert.Single(blackboard.chooseBaitFailures.Where(f => f == "fake fly bait")); + Assert.Single(blackboard.chooseBaitFailures.Where(f => f == BaitType.FakeFlyBait)); #endregion #region koi受到遮挡,第2次失败 @@ -254,9 +254,9 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests actual = sut.Tick(imageRegion); // - Assert.True(String.IsNullOrEmpty(blackboard.selectedBaitName)); + Assert.Null(blackboard.selectedBait); Assert.Equal(BehaviourStatus.Failed, actual); - Assert.Single(blackboard.chooseBaitFailures.Where(f => f == "spinelgrain bait")); + Assert.Single(blackboard.chooseBaitFailures.Where(f => f == BaitType.SpinelgrainBait)); Assert.False(blackboard.abort); #endregion @@ -273,9 +273,9 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests actual = sut.Tick(imageRegion); // - Assert.True(String.IsNullOrEmpty(blackboard.selectedBaitName)); + Assert.Null(blackboard.selectedBait); Assert.Equal(BehaviourStatus.Failed, actual); - Assert.Equal(2, blackboard.chooseBaitFailures.Where(f => f == "fake fly bait").Count()); + Assert.Equal(2, blackboard.chooseBaitFailures.Where(f => f == BaitType.FakeFlyBait).Count()); #endregion #region 第4次失败 @@ -291,9 +291,9 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests actual = sut.Tick(imageRegion); // - Assert.True(String.IsNullOrEmpty(blackboard.selectedBaitName)); + Assert.Null(blackboard.selectedBait); Assert.Equal(BehaviourStatus.Failed, actual); - Assert.Equal(2, blackboard.chooseBaitFailures.Where(f => f == "spinelgrain bait").Count()); + Assert.Equal(2, blackboard.chooseBaitFailures.Where(f => f == BaitType.SpinelgrainBait).Count()); #endregion } } diff --git a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFishingTests/BehavioursTests.FishBite.cs b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFishingTests/BehavioursTests.FishBite.cs index 7ce722de..b2659101 100644 --- a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFishingTests/BehavioursTests.FishBite.cs +++ b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFishingTests/BehavioursTests.FishBite.cs @@ -1,4 +1,4 @@ -using BehaviourTree; +using BehaviourTree; using BetterGenshinImpact.GameTask.AutoFishing; using BetterGenshinImpact.GameTask.Model.Area.Converter; using BetterGenshinImpact.GameTask.Model.Area; @@ -6,8 +6,6 @@ using BehaviourTree.Composites; using BehaviourTree.FluentBuilder; using Microsoft.Extensions.Time.Testing; using OpenCvSharp; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Localization; namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests { @@ -106,17 +104,12 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests Mat mat = new Mat(@$"..\..\..\Assets\AutoFishing\{screenshot1080p}"); var imageRegion = new GameCaptureRegion(mat, 0, 0, new DesktopRegion(new FakeMouseSimulator()), converter: new ScaleConverter(1d), drawContent: new FakeDrawContent()); - ServiceCollection services = new ServiceCollection(); - services.AddLogging().AddLocalization(); - using ServiceProvider sp = services.BuildServiceProvider(); - IStringLocalizer autoFishingImageRecognitionStringLocalizer = sp.GetRequiredService>(); - FakeSystemInfo systemInfo = new FakeSystemInfo(new Vanara.PInvoke.RECT(0, 0, mat.Width, mat.Height), 1); AutoFishingAssets autoFishingAssets = new AutoFishingAssets(systemInfo); Blackboard blackboard = new Blackboard(autoFishingAssets: autoFishingAssets); // - FishBite sut = new FishBite("-", blackboard, new FakeLogger(), false, new FakeInputSimulator(), OcrService, drawContent: new FakeDrawContent(), new System.Globalization.CultureInfo(cultureName), autoFishingImageRecognitionStringLocalizer); + FishBite sut = new FishBite("-", blackboard, new FakeLogger(), false, new FakeInputSimulator(), OcrService, drawContent: new FakeDrawContent(), new System.Globalization.CultureInfo(cultureName), stringLocalizer); BehaviourStatus actual = sut.Tick(imageRegion); // diff --git a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFishingTests/BehavioursTests.GetFishpond.cs b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFishingTests/BehavioursTests.GetFishpond.cs index 80a9f460..c0615176 100644 --- a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFishingTests/BehavioursTests.GetFishpond.cs +++ b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFishingTests/BehavioursTests.GetFishpond.cs @@ -1,8 +1,9 @@ -using BetterGenshinImpact.GameTask.AutoFishing; +using BetterGenshinImpact.GameTask.AutoFishing; using BehaviourTree; using BetterGenshinImpact.GameTask.Model.Area; using Microsoft.Extensions.Time.Testing; using OpenCvSharp; +using BetterGenshinImpact.GameTask.AutoFishing.Model; namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests { @@ -38,10 +39,10 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests } [Theory] - [InlineData("20250225101257889_GetFishpond_Succeeded.png", new string[] { "fruit paste bait", "fruit paste bait", "redrot bait", "redrot bait" }, new string[] { "false worm bait", "false worm bait", "fake fly bait", "fake fly bait" })] + [InlineData("20250225101257889_GetFishpond_Succeeded.png", new BaitType[] { BaitType.FruitPasteBait, BaitType.FruitPasteBait, BaitType.RedrotBait, BaitType.RedrotBait }, new BaitType[] { BaitType.FalseWormBait, BaitType.FalseWormBait, BaitType.FakeFlyBait, BaitType.FakeFlyBait })] /// 测试鱼的鱼饵均在失败列表中且被忽略,结果为运行中 /// - public void GetFishpondTest_AllIgnored_ShouldBeRunning(string screenshot1080p, IEnumerable chooseBaitfailures, IEnumerable throwRodNoTargetFishfailures) + public void GetFishpondTest_AllIgnored_ShouldBeRunning(string screenshot1080p, IEnumerable chooseBaitfailures, IEnumerable throwRodNoTargetFishfailures) { // Mat mat = new Mat(@$"..\..\..\Assets\AutoFishing\{screenshot1080p}"); diff --git a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFishingTests/BehavioursTests.ThrowRod.cs b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFishingTests/BehavioursTests.ThrowRod.cs index 0850d817..5c0bc914 100644 --- a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFishingTests/BehavioursTests.ThrowRod.cs +++ b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFishingTests/BehavioursTests.ThrowRod.cs @@ -1,4 +1,4 @@ -using BehaviourTree; +using BehaviourTree; using BetterGenshinImpact.GameTask.AutoFishing; using BetterGenshinImpact.GameTask.Model.Area.Converter; using BetterGenshinImpact.GameTask.Model.Area; @@ -6,18 +6,19 @@ using Microsoft.Extensions.Time.Testing; using OpenCvSharp; using BehaviourTree.Composites; using BehaviourTree.FluentBuilder; +using BetterGenshinImpact.GameTask.AutoFishing.Model; namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests { public partial class BehavioursTests { [Theory] - [InlineData(@"20250225101304534_ThrowRod_Succeeded.png", "false worm bait")] - [InlineData(@"20250226162217468_ThrowRod_Succeeded.png", "fruit paste bait")] + [InlineData(@"20250225101304534_ThrowRod_Succeeded.png", BaitType.FalseWormBait)] + [InlineData(@"20250226162217468_ThrowRod_Succeeded.png", BaitType.FruitPasteBait)] /// /// 测试各种抛竿,结果为成功 /// - public void ThrowRodTest_VariousFish_ShouldSuccess(string screenshot1080p, string selectedBaitName) + public void ThrowRodTest_VariousFish_ShouldSuccess(string screenshot1080p, BaitType selectedBait) { // Mat mat = new Mat(@$"..\..\..\Assets\AutoFishing\{screenshot1080p}"); @@ -25,7 +26,7 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests var blackboard = new Blackboard(Predictor, sleep: i => { }) { - selectedBaitName = selectedBaitName + selectedBait = selectedBait }; FakeTimeProvider fakeTimeProvider = new FakeTimeProvider(); @@ -40,12 +41,12 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests } [Theory] - [InlineData(@"20250225101304534_ThrowRod_Succeeded.png", "redrot bait")] - [InlineData(@"20250225101304534_ThrowRod_Succeeded.png", "fake fly bait")] + [InlineData(@"20250225101304534_ThrowRod_Succeeded.png", BaitType.RedrotBait)] + [InlineData(@"20250225101304534_ThrowRod_Succeeded.png", BaitType.FakeFlyBait)] /// /// 测试各种抛竿,未满足HutaoFisher判定,结果为运行中 /// - public void ThrowRodTest_VariousFish_ShouldFail(string screenshot1080p, string selectedBaitName) + public void ThrowRodTest_VariousFish_ShouldFail(string screenshot1080p, BaitType selectedBait) { // Mat mat = new Mat(@$"..\..\..\Assets\AutoFishing\{screenshot1080p}"); @@ -53,7 +54,7 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests var blackboard = new Blackboard(Predictor, sleep: i => { }) { - selectedBaitName = selectedBaitName + selectedBait = selectedBait }; FakeTimeProvider fakeTimeProvider = new FakeTimeProvider(); @@ -68,11 +69,11 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests } [Theory] - [InlineData(@"20250225101304534_ThrowRod_Succeeded.png", "flashing maintenance mek bait")] + [InlineData(@"20250225101304534_ThrowRod_Succeeded.png", BaitType.FlashingMaintenanceMekBait)] /// /// 测试各种抛竿,无鱼饵适用鱼,结果为失败 /// - public void ThrowRodTest_NoBaitFish_ShouldFail(string screenshot1080p, string selectedBaitName) + public void ThrowRodTest_NoBaitFish_ShouldFail(string screenshot1080p, BaitType selectedBait) { // Mat mat = new Mat(@$"..\..\..\Assets\AutoFishing\{screenshot1080p}"); @@ -80,7 +81,7 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests var blackboard = new Blackboard(Predictor, sleep: i => { }) { - selectedBaitName = selectedBaitName + selectedBait = selectedBait }; FakeTimeProvider fakeTimeProvider = new FakeTimeProvider(); @@ -119,7 +120,7 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests var blackboard = new Blackboard(Predictor, sleep: i => { }) { - selectedBaitName = "fake fly bait" + selectedBait = GameTask.AutoFishing.Model.BaitType.FakeFlyBait }; // diff --git a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFishingTests/BehavioursTests.cs b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFishingTests/BehavioursTests.cs index b71c763a..7507bee7 100644 --- a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFishingTests/BehavioursTests.cs +++ b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFishingTests/BehavioursTests.cs @@ -1,7 +1,10 @@ -using BetterGenshinImpact.Core.Config; using BetterGenshinImpact.Core.Recognition.OCR; using BetterGenshinImpact.Core.Recognition.ONNX; +using BetterGenshinImpact.GameTask.AutoFishing; using BetterGenshinImpact.UnitTest.CoreTests.RecognitionTests.OCRTests; +using BetterGenshinImpact.UnitTest.GameTaskTests.GetGridIconsTests; +using Microsoft.Extensions.Localization; +using Microsoft.ML.OnnxRuntime; namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests { @@ -12,12 +15,19 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests private static BgiYoloPredictor predictor; #pragma warning restore CS8618 // 在退出构造函数时,不可为 null 的字段必须包含非 null 值。请考虑添加 "required" 修饰符或声明为可为 null。 - private readonly PaddleFixture paddle; + private readonly PaddleFixture paddle; + private readonly IStringLocalizer stringLocalizer; + private readonly InferenceSession session; + private readonly Dictionary prototypes; - public BehavioursTests(PaddleFixture paddle, TorchFixture torch) + + public BehavioursTests(PaddleFixture paddle, TorchFixture torch, LocalizationFixture localization, GridIconModelFixture iconModel) { this.paddle = paddle; this.useTorch = torch.UseTorch; + this.stringLocalizer = localization.CreateStringLocalizer(); + this.session = iconModel.modelLoader.Value.session; + this.prototypes = iconModel.modelLoader.Value.prototypes; } private IOcrService OcrService => paddle.Get(); diff --git a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/GetGridIconsTests/GetGridIconsTaskTest.cs b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/GetGridIconsTests/GetGridIconsTaskTests.cs similarity index 88% rename from Test/BetterGenshinImpact.UnitTest/GameTaskTests/GetGridIconsTests/GetGridIconsTaskTest.cs rename to Test/BetterGenshinImpact.UnitTest/GameTaskTests/GetGridIconsTests/GetGridIconsTaskTests.cs index 3d632d16..7d6a8a1d 100644 --- a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/GetGridIconsTests/GetGridIconsTaskTest.cs +++ b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/GetGridIconsTests/GetGridIconsTaskTests.cs @@ -1,4 +1,4 @@ -using BetterGenshinImpact.GameTask.GetGridIcons; +using BetterGenshinImpact.GameTask.GetGridIcons; using OpenCvSharp; using System; using System.Collections.Generic; @@ -8,7 +8,7 @@ using System.Threading.Tasks; namespace BetterGenshinImpact.UnitTest.GameTaskTests.GetGridIconsTests { - public class GetGridIconsTaskTest + public class GetGridIconsTaskTests { [Theory] [InlineData(@"AutoArtifactSalvage\ArtifactAffixes.png", 5)] diff --git a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/GetGridIconsTests/GridIconModelFixture.cs b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/GetGridIconsTests/GridIconModelFixture.cs new file mode 100644 index 00000000..656ed088 --- /dev/null +++ b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/GetGridIconsTests/GridIconModelFixture.cs @@ -0,0 +1,25 @@ +using BetterGenshinImpact.GameTask.GetGridIcons; +using Microsoft.ML.OnnxRuntime; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BetterGenshinImpact.UnitTest.GameTaskTests.GetGridIconsTests +{ + public class GridIconModelFixture + { + internal readonly Lazy modelLoader = new Lazy(); + } + + internal class ModelLoader + { + internal readonly InferenceSession session; + internal readonly Dictionary prototypes; + public ModelLoader() + { + this.session = GridIconsAccuracyTestTask.LoadModel(out this.prototypes); + } + } +} diff --git a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/GetGridIconsTests/GridIconsAccuracyTestTaskTests.cs b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/GetGridIconsTests/GridIconsAccuracyTestTaskTests.cs new file mode 100644 index 00000000..ece8e548 --- /dev/null +++ b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/GetGridIconsTests/GridIconsAccuracyTestTaskTests.cs @@ -0,0 +1,69 @@ +using BetterGenshinImpact.GameTask.GetGridIcons; +using BetterGenshinImpact.GameTask.Model.GameUI; +using Microsoft.ML.OnnxRuntime; +using OpenCvSharp; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BetterGenshinImpact.UnitTest.GameTaskTests.GetGridIconsTests +{ + [Collection("Init Collection")] + public class GridIconsAccuracyTestTaskTests + { + private readonly InferenceSession session; + private readonly Dictionary prototypes; + + public GridIconsAccuracyTestTaskTests(GridIconModelFixture model) + { + this.session = model.modelLoader.Value.session; + this.prototypes = model.modelLoader.Value.prototypes; + } + + public static IEnumerable InferTestData() + { + 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) } }; + // string.Join(", ",result.Select(s=>$"(\"{s.Item1}\", {s.Item2})")) + } + + [Theory] + [MemberData(nameof(InferTestData))] + /// + /// 测试推理图标的标签,结果应正确 + /// + public void Infer_ShouldBeRight(string screenshot, int columns, bool findContoursAlpha, (string, int)[] nameAndStars) + { + // + using Mat mat = new Mat(@$"..\..\..\Assets\{screenshot}"); + + var gridItems = GridScreen.GridEnumerator.GetGridItems(mat, columns, findContoursAlpha: findContoursAlpha); + var rows = GridScreen.GridEnumerator.ClusterRows(gridItems, 10); + + // + var result = new List<(string, int)>(); + foreach (var row in rows) + { + foreach (Rect rect in row) + { + Mat gridItemMat = mat.SubMat(rect); + using Mat icon = gridItemMat.GetGridIcon(); + var pred = GridIconsAccuracyTestTask.Infer(icon, this.session, this.prototypes); + result.Add(pred); + } + } + + // + for (int i = 0; i < nameAndStars.Length - 1; i++) + { + Assert.Equal(nameAndStars[i].Item1, result[i].Item1); + Assert.Equal(nameAndStars[i].Item2, result[i].Item2); + } + } + } +} diff --git a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/Model/GameUI/ArtifactSetFilterScreenTests.cs b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/Model/GameUI/ArtifactSetFilterScreenTests.cs new file mode 100644 index 00000000..b6ff9b3a --- /dev/null +++ b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/Model/GameUI/ArtifactSetFilterScreenTests.cs @@ -0,0 +1,31 @@ +using BetterGenshinImpact.GameTask.Model.GameUI; +using OpenCvSharp; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BetterGenshinImpact.UnitTest.GameTaskTests.Model.GameUI +{ + public class ArtifactSetFilterScreenTests + { + [Theory] + [InlineData(@"GameUI\ArtifactSetFilterBright.png", 20, 2)] + [InlineData(@"GameUI\ArtifactSetFilterDark.png", 20, 2)] + /// + /// 测试获取圣遗物套装筛选界面中的项目,结果应正确 + /// + public void GetGridItems_ShouldBeRight(string screenshot, int count, int columns) + { + // + using Mat mat = new Mat(@$"..\..\..\Assets\{screenshot}"); + + // + var result = ArtifactSetFilterScreen.GetGridItems(mat, columns); + + // + Assert.Equal(count, result.Count()); + } + } +} diff --git a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/Model/GameUI/GridScreenTests.cs b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/Model/GameUI/GridScreenTests.cs index 1f9ed583..23cb235d 100644 --- a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/Model/GameUI/GridScreenTests.cs +++ b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/Model/GameUI/GridScreenTests.cs @@ -1,4 +1,5 @@ -using BetterGenshinImpact.GameTask.Model.GameUI; +using BetterGenshinImpact.GameTask.Model.GameUI; +using BetterGenshinImpact.UnitTest.CoreTests.RecognitionTests.OCRTests; using OpenCvSharp; using System; using System.Collections.Generic; @@ -8,8 +9,15 @@ using System.Threading.Tasks; namespace BetterGenshinImpact.UnitTest.GameTaskTests.Model.GameUI { + [Collection("Init Collection")] public class GridScreenTests { + private readonly PaddleFixture paddle; + public GridScreenTests(PaddleFixture paddle) + { + this.paddle = paddle; + } + [Theory] [InlineData(@"AutoArtifactSalvage\ArtifactGrid.png", 4, 2)] [InlineData(@"GetGridIcons\WeaponGrid3.png", 32, 8)] @@ -62,9 +70,9 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.Model.GameUI cropped.CopyTo(pos); // - bool result1 = GridScreen.GridEnumerator.IsScrolling(mat, scrolled, out Point2d shift, upperThreshold: 0.99); - bool result2 = GridScreen.GridEnumerator.IsScrolling(mat, mat, out Point2d _); - bool result3 = GridScreen.GridEnumerator.IsScrolling(mat, black, out Point2d _); + bool result1 = GridScroller.IsScrolling(mat, scrolled, out Point2d shift, upperThreshold: 0.99); + bool result2 = GridScroller.IsScrolling(mat, mat, out Point2d _); + bool result3 = GridScroller.IsScrolling(mat, black, out Point2d _); // Assert.True(result1); @@ -72,5 +80,46 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.Model.GameUI Assert.False(result2); Assert.False(result3); } + + public static IEnumerable GetGridItemIconTextTestData() + { + yield return new object[] { @"GetGridIcons\FoodGrid.png", 8, new[] { 1850, 1073, 167, 85, 69, 6, 1, 90, + 6, 5, 1, 10, 15, 3, 2, 1, + 2, 2, 2, 1, 3, 2, 2, 4, + 1, 1, 3, 2, 2, 1, 1, 1} }; + } + + [Theory] + [MemberData(nameof(GetGridItemIconTextTestData))] + /// + /// 测试获取各种界面中的物品图标文字,全部转数字,结果应正确 + /// + public void GetGridItemIconText_Number_ShouldBeRight(string screenshot, int columns, int[] numbers) + { + // + using Mat mat = new Mat(@$"..\..\..\Assets\{screenshot}"); + + // + var gridItems = GridScreen.GridEnumerator.GetGridItems(mat, columns, findContoursAlpha: true); + var rows = GridScreen.GridEnumerator.ClusterRows(gridItems, 10); + + var result = new List(); + foreach (var row in rows) + { + foreach (Rect rect in row) + { + Mat gridItemMat = mat.SubMat(rect); + string numStr = gridItemMat.GetGridItemIconText(paddle.Get()); + result.Add(numStr); + } + } + + // + for (int i = 0; i < numbers.Length - 1; i++) + { + Assert.True(int.TryParse(result[i], out int intResult), $"第{i + 1}个图标文字解析失败-->{result[i]}<--"); + Assert.Equal(numbers[i], intResult); + } + } } } diff --git a/Test/BetterGenshinImpact.UnitTest/InitCollection.cs b/Test/BetterGenshinImpact.UnitTest/InitCollection.cs index eb7f0f7b..1e164f76 100644 --- a/Test/BetterGenshinImpact.UnitTest/InitCollection.cs +++ b/Test/BetterGenshinImpact.UnitTest/InitCollection.cs @@ -1,5 +1,6 @@ -using BetterGenshinImpact.UnitTest.CoreTests.RecognitionTests.OCRTests; +using BetterGenshinImpact.UnitTest.CoreTests.RecognitionTests.OCRTests; using BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests; +using BetterGenshinImpact.UnitTest.GameTaskTests.GetGridIconsTests; using System; using System.Collections.Generic; using System.Linq; @@ -9,7 +10,7 @@ using System.Threading.Tasks; namespace BetterGenshinImpact.UnitTest { [CollectionDefinition("Init Collection")] - public class InitCollection : ICollectionFixture, ICollectionFixture + public class InitCollection : ICollectionFixture, ICollectionFixture, ICollectionFixture, ICollectionFixture { } } diff --git a/Test/BetterGenshinImpact.UnitTest/LocalizationFixture.cs b/Test/BetterGenshinImpact.UnitTest/LocalizationFixture.cs new file mode 100644 index 00000000..6d430400 --- /dev/null +++ b/Test/BetterGenshinImpact.UnitTest/LocalizationFixture.cs @@ -0,0 +1,29 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BetterGenshinImpact.UnitTest +{ + public class LocalizationFixture + { + private readonly ServiceProvider sp; + public LocalizationFixture() + { + var services = new ServiceCollection(); + services.AddSingleton() + .AddLocalization(); + this.sp = services.BuildServiceProvider(); + } + + public IStringLocalizer CreateStringLocalizer() + { + return this.sp.GetRequiredService>(); + } + } +}