diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1c297620..cdac0706 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -74,7 +74,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - repository: huiyadanli/bettergi-scripts-web + repository: zaodonganqi/bettergi-script-web - uses: actions/setup-node@v4 with: node-version: 20 diff --git a/BetterGenshinImpact/BetterGenshinImpact.csproj b/BetterGenshinImpact/BetterGenshinImpact.csproj index b225d152..6854ec3c 100644 --- a/BetterGenshinImpact/BetterGenshinImpact.csproj +++ b/BetterGenshinImpact/BetterGenshinImpact.csproj @@ -2,7 +2,7 @@ BetterGI - 0.51.1-alpha.1 + 0.53.1-alpha.4 false WinExe net8.0-windows10.0.22621.0 @@ -41,11 +41,12 @@ + - - - + + + @@ -53,6 +54,7 @@ + @@ -64,6 +66,7 @@ + diff --git a/BetterGenshinImpact/BetterGenshinImpact_zooefysz_wpftmp.csproj b/BetterGenshinImpact/BetterGenshinImpact_zooefysz_wpftmp.csproj new file mode 100644 index 00000000..53f3a802 --- /dev/null +++ b/BetterGenshinImpact/BetterGenshinImpact_zooefysz_wpftmp.csproj @@ -0,0 +1,588 @@ + + + BetterGI + obj\x64\Debug\ + obj\ + D:\HuiPrograming\Projects\CSharp\MiHoYo\BetterGenshinImpact\BetterGenshinImpact\obj\ + <_TargetAssemblyProjectName>BetterGenshinImpact + BetterGenshinImpact + + + + 0.51.1-alpha.1 + false + WinExe + net8.0-windows10.0.22621.0 + enable + true + true + 12.0 + true + Resources\Images\logo.ico + x64 + embedded + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + + + + + + + + + + + + Code + WelcomeDialog.xaml + + + + <_DeploymentManifestIconFile Remove="Resources\Images\logo.ico" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/BetterGenshinImpact/Core/Config/CommonConfig.cs b/BetterGenshinImpact/Core/Config/CommonConfig.cs index 7510aa1f..338c3d64 100644 --- a/BetterGenshinImpact/Core/Config/CommonConfig.cs +++ b/BetterGenshinImpact/Core/Config/CommonConfig.cs @@ -73,4 +73,11 @@ public partial class CommonConfig : ObservableObject /// [ObservableProperty] private List _onceHadRunDeviceIdList = new(); + + + /// + /// 当前看过的兑换码推送版本 + /// + [ObservableProperty] + private string _redeemCodeFeedsUpdateVersion = "20251013"; } diff --git a/BetterGenshinImpact/Core/Config/MaskWindowConfig.cs b/BetterGenshinImpact/Core/Config/MaskWindowConfig.cs index 968994dc..11995594 100644 --- a/BetterGenshinImpact/Core/Config/MaskWindowConfig.cs +++ b/BetterGenshinImpact/Core/Config/MaskWindowConfig.cs @@ -69,4 +69,10 @@ public partial class MaskWindowConfig : ObservableObject /// [ObservableProperty] private bool _useSubform = false; + + /// + /// 遮罩文本透明度 (0.0-1.0) + /// + [ObservableProperty] + private double _textOpacity = 1.0; } diff --git a/BetterGenshinImpact/Core/Config/PathingConditionConfig.cs b/BetterGenshinImpact/Core/Config/PathingConditionConfig.cs index 591c796d..d7b7261b 100644 --- a/BetterGenshinImpact/Core/Config/PathingConditionConfig.cs +++ b/BetterGenshinImpact/Core/Config/PathingConditionConfig.cs @@ -6,6 +6,10 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; +using BetterGenshinImpact.GameTask; +using BetterGenshinImpact.GameTask.AutoPathing.Model; +using BetterGenshinImpact.GameTask.Common; +using Microsoft.Extensions.Logging; namespace BetterGenshinImpact.Core.Config; diff --git a/BetterGenshinImpact/Core/Config/ScriptConfig.cs b/BetterGenshinImpact/Core/Config/ScriptConfig.cs index 0f6aaead..d9e93a9a 100644 --- a/BetterGenshinImpact/Core/Config/ScriptConfig.cs +++ b/BetterGenshinImpact/Core/Config/ScriptConfig.cs @@ -1,6 +1,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using System; using System.Collections.Generic; +using System.Windows; namespace BetterGenshinImpact.Core.Config; @@ -25,7 +26,25 @@ public partial class ScriptConfig : ObservableObject // 已订阅的脚本路径列表 [ObservableProperty] private List _subscribedScriptPaths = []; + + // 选择的更新渠道名称 + [ObservableProperty] private string _selectedChannelName = ""; + + // 自定义渠道的URL + [ObservableProperty] private string _customRepoUrl = ""; - // 选择的更新渠道URL - [ObservableProperty] private string _selectedRepoUrl = ""; + // 仓库页面宽度 + [ObservableProperty] private double _webviewWidth = 0; + + // 仓库页面高度 + [ObservableProperty] private double _webviewHeight = 0; + + // 仓库页面横向位置 + [ObservableProperty] private double _webviewLeft = 0; + + // 仓库页面纵向位置 + [ObservableProperty] private double _webviewTop = 0; + + // 仓库页面是否最大化 + [ObservableProperty] private WindowState _webviewState = WindowState.Normal; } diff --git a/BetterGenshinImpact/Core/Recognition/OpenCv/ImageDifferenceDetector.cs b/BetterGenshinImpact/Core/Recognition/OpenCv/ImageDifferenceDetector.cs new file mode 100644 index 00000000..e88a8a20 --- /dev/null +++ b/BetterGenshinImpact/Core/Recognition/OpenCv/ImageDifferenceDetector.cs @@ -0,0 +1,95 @@ +using System; +using System.Diagnostics; +using OpenCvSharp; + +namespace BetterGenshinImpact.Core.Recognition.OpenCv; + +public class ImageDifferenceDetector +{ + /// + /// 找出四张灰度图中与其他三张差异最大的图片 + /// 投票机制:每张图片投票给与它差异最大的图片,如果某张图片获得3票则返回其索引 + /// + /// 包含4张Mat对象的数组 + /// 差异最大的图片索引(0-3),如果没有图片获得3票则返回-1 + public static int FindMostDifferentImage(Mat[] images) + { + if (images is not { Length: 4 }) + { + throw new ArgumentException("必须提供4张图片"); + } + + // 验证所有图片尺寸相同 + Size firstSize = images[0].Size(); + for (int i = 1; i < 4; i++) + { + if (images[i].Size() != firstSize) + { + throw new ArgumentException("所有图片必须具有相同的尺寸"); + } + } + + // 存储每张图片获得的投票数 + int[] votes = new int[4]; + + // 每张图片投票给与它差异最大的图片 + for (int i = 0; i < 4; i++) + { + int maxDiffImageIndex = -1; + double maxDifference = 0; + + // 找出与图片i差异最大的图片 + for (int j = 0; j < 4; j++) + { + if (i != j) + { + double difference = CalculateDifference(images[i], images[j]); + Debug.WriteLine($"{i} vs {j} 差异像素数量: {difference}"); + + if (difference > maxDifference) + { + maxDifference = difference; + maxDiffImageIndex = j; + } + } + } + + // 图片i投票给差异最大的图片 + if (maxDiffImageIndex != -1) + { + votes[maxDiffImageIndex]++; + Debug.WriteLine($"图片 {i} 投票给图片 {maxDiffImageIndex} (差异值: {maxDifference:F2})"); + } + } + + // 输出投票结果 + Debug.WriteLine("\n投票结果:"); + for (int i = 0; i < 4; i++) + { + Debug.WriteLine($"图片 {i}: {votes[i]} 票"); + } + + // 检查是否有图片获得3票 + for (int i = 0; i < 4; i++) + { + if (votes[i] >= 3) + { + return i; + } + } + + return -1; + } + + /// + /// 计算差值 + /// + private static double CalculateDifference(Mat img1, Mat img2) + { + using var diff = new Mat(); + // 计算差值的绝对值 + Cv2.Absdiff(img1, img2, diff); + // 统计非零像素点(即颜色不同的像素点) + return Cv2.CountNonZero(diff); + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/Core/Recognition/RecognitionObject.cs b/BetterGenshinImpact/Core/Recognition/RecognitionObject.cs index 7e7015cd..0625e142 100644 --- a/BetterGenshinImpact/Core/Recognition/RecognitionObject.cs +++ b/BetterGenshinImpact/Core/Recognition/RecognitionObject.cs @@ -84,6 +84,16 @@ public class RecognitionObject /// public int MaxMatchCount { get; set; } = -1; + /// + /// 是否启用二值化后模板匹配 + /// + public bool UseBinaryMatch { get; set; } = false; + + /// + /// 二值化阈值,默认 128 + /// + public int BinaryThreshold { get; set; } = 128; + public RecognitionObject InitTemplate() { if (TemplateImageMat != null && TemplateImageGreyMat == null) diff --git a/BetterGenshinImpact/Core/Script/Dependence/Dispatcher.cs b/BetterGenshinImpact/Core/Script/Dependence/Dispatcher.cs index f43a0d6b..dd7e7bc6 100644 --- a/BetterGenshinImpact/Core/Script/Dependence/Dispatcher.cs +++ b/BetterGenshinImpact/Core/Script/Dependence/Dispatcher.cs @@ -18,6 +18,7 @@ using System.Collections.Generic; using System.Dynamic; using System.Threading; using System.Threading.Tasks; +using BetterGenshinImpact.GameTask.AutoFight; namespace BetterGenshinImpact.Core.Script.Dependence; @@ -304,4 +305,38 @@ public class Dispatcher { return GetLinkedCancellationTokenSource().Token; } + + /// + /// 运行自动秘境任务 + /// + /// 秘境任务参数 + /// 自定义取消令牌 + /// + public async Task RunAutoDomainTask(AutoDomainParam param, CancellationToken? customCt = null) + { + if (param == null) + { + throw new ArgumentNullException(nameof(param), "秘境任务参数不能为空"); + } + + CancellationToken cancellationToken = customCt ?? CancellationContext.Instance.Cts.Token; + await new AutoDomainTask(param).Start(cancellationToken); + } + + /// + /// 运行自动战斗任务 + /// + /// 战斗任务参数 + /// 自定义取消令牌 + /// + public async Task RunAutoFightTask(AutoFightParam param, CancellationToken? customCt = null) + { + if (param == null) + { + throw new ArgumentNullException(nameof(param), "战斗任务参数不能为空"); + } + + CancellationToken cancellationToken = customCt ?? CancellationContext.Instance.Cts.Token; + await new AutoFightTask(param).Start(cancellationToken); + } } diff --git a/BetterGenshinImpact/Core/Script/Dependence/Genshin.cs b/BetterGenshinImpact/Core/Script/Dependence/Genshin.cs index dfd17584..d1d5625e 100644 --- a/BetterGenshinImpact/Core/Script/Dependence/Genshin.cs +++ b/BetterGenshinImpact/Core/Script/Dependence/Genshin.cs @@ -45,7 +45,8 @@ public class Genshin public Lazy LazyNavigationInstance { get; } = new(() => { - Navigation.WarmUp(); + var matchingMethod = TaskContext.Instance().Config.PathingConditionConfig.MapMatchingMethod; + Navigation.WarmUp(matchingMethod); return new NavigationInstance(); }); @@ -227,7 +228,10 @@ public class Genshin throw new InvalidOperationException("不在主界面,无法识别小地图坐标"); } - return MapManager.GetMap(mapName).ConvertImageCoordinatesToGenshinMapCoordinates(LazyNavigationInstance.Value.GetPositionStableByCache(imageRegion, mapName, cacheTimeMs)); + var matchingMethod = TaskContext.Instance().Config.PathingConditionConfig.MapMatchingMethod; + return MapManager.GetMap(mapName, matchingMethod) + .ConvertImageCoordinatesToGenshinMapCoordinates(LazyNavigationInstance.Value + .GetPositionStableByCache(imageRegion, mapName, matchingMethod, cacheTimeMs)); } /// @@ -244,11 +248,12 @@ public class Genshin { throw new InvalidOperationException("不在主界面,无法识别小地图坐标"); } - var sceneMap = MapManager.GetMap(mapName); + var matchingMethod = TaskContext.Instance().Config.PathingConditionConfig.MapMatchingMethod; + var sceneMap = MapManager.GetMap(mapName, matchingMethod); var navigationInstance = LazyNavigationInstance.Value; var pos = sceneMap.ConvertGenshinMapCoordinatesToImageCoordinates(new Point2f(x, y)); navigationInstance.SetPrevPosition(pos.X, pos.Y); - return sceneMap.ConvertImageCoordinatesToGenshinMapCoordinates(navigationInstance.GetPosition(imageRegion, mapName)); + return sceneMap.ConvertImageCoordinatesToGenshinMapCoordinates(navigationInstance.GetPosition(imageRegion, mapName, matchingMethod)); } #endregion 大地图操作 @@ -372,4 +377,36 @@ public class Genshin { await new ExitAndReloginJob().Start(CancellationContext.Instance.Cts.Token); } + + /// + /// 调整时间 + /// + /// 目标小时(0-24) + /// 目标分钟(0-59) + /// 是否跳过动画(默认为否) + /// + public async Task SetTime(int hour, int minute, bool skip = false) + { + if ( hour < 0 || hour > 24) + throw new ArgumentException($"无效的小时值: {hour},必须是 0-24 之间的整数字符", nameof(hour)); + if (minute < 0 || minute > 59) + throw new ArgumentException($"无效的分钟值: {minute},必须是 0-59 之间的整数字符", nameof(minute)); + await new SetTimeTask().Start(hour, minute, CancellationContext.Instance.Cts.Token, skip); + } + + /// + /// 调整时间 + /// + /// 目标小时(0-24的字符类型) + /// 目标分钟(0-59的字符类型) + /// 是否跳过动画(默认为否) + /// + public async Task SetTime(string hour, string minute, bool skip = false) + { + if (!int.TryParse(hour, out var h) || h < 0 || h > 24) + throw new ArgumentException($"无效的小时值: {hour},必须是 0-24 之间的整数字符", nameof(hour)); + if (!int.TryParse(minute, out var m) || m < 0 || m > 59) + throw new ArgumentException($"无效的分钟值: {minute},必须是 0-59 之间的整数字符", nameof(minute)); + await new SetTimeTask().Start(h, m, CancellationContext.Instance.Cts.Token, skip); + } } \ No newline at end of file diff --git a/BetterGenshinImpact/Core/Script/Dependence/GlobalMethod.cs b/BetterGenshinImpact/Core/Script/Dependence/GlobalMethod.cs index b54d2b25..ea8678ae 100644 --- a/BetterGenshinImpact/Core/Script/Dependence/GlobalMethod.cs +++ b/BetterGenshinImpact/Core/Script/Dependence/GlobalMethod.cs @@ -161,6 +161,11 @@ public class GlobalMethod _dpi = dpi; } + public static double[] GetGameMetrics() + { + return [_gameWidth, _gameHeight, _dpi]; + } + public static void MoveMouseBy(int x, int y) { var realDpi = TaskContext.Instance().DpiScale; diff --git a/BetterGenshinImpact/Core/Script/Dependence/Http.cs b/BetterGenshinImpact/Core/Script/Dependence/Http.cs new file mode 100644 index 00000000..7dc449ad --- /dev/null +++ b/BetterGenshinImpact/Core/Script/Dependence/Http.cs @@ -0,0 +1,116 @@ +using BetterGenshinImpact.Core.Script.Utils; +using System; +using System.IO; +using System.Threading.Tasks; +using OpenCvSharp; +using System.Linq; +using System.Text.Json; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using BetterGenshinImpact.GameTask; +using Microsoft.Extensions.Logging; + +namespace BetterGenshinImpact.Core.Script.Dependence; + +public class Http +{ + private readonly ILogger _logger = App.GetLogger(); + + private void CheckHttpPermission(string url) + { + var currentProject = TaskContext.Instance().CurrentScriptProject; + if (!currentProject?.AllowJsHTTP ?? false) + { + throw new UnauthorizedAccessException("当前JS脚本不允许使用HTTP请求,请在调度器通用设置中启用“JS HTTP权限”"); + } + var allowedUrls = currentProject?.Project?.Manifest.HttpAllowedUrls ?? []; + if (allowedUrls.Length == 0) + { + throw new UnauthorizedAccessException("当前JS脚本没有配置允许请求的URL,请在脚本的manifest.json中配置http_allowed_urls"); + } + if (allowedUrls.Any(allowedUrl => + { + // fuzzy match + var pattern = "^" + System.Text.RegularExpressions.Regex.Escape(allowedUrl).Replace("\\*", ".*") + "$"; + _logger.LogDebug($"[HTTP] 检查URL {url} 是否符合: {pattern}"); + var regex = new System.Text.RegularExpressions.Regex(pattern); + return regex.IsMatch(url); + })) + { + return; + } + throw new UnauthorizedAccessException($"当前JS脚本不允许请求此URL: {url},请在脚本的manifest.json中配置http_allowed_urls,当前允许的URL列表: [{string.Join(", ", allowedUrls)}]"); + } + + public class HttpReponse + { + public int status_code { get; set; } + public Dictionary headers { get; set; } = new(); + public string body { get; set; } = ""; + } + + + /// + /// 执行HTTP请求 + /// + /// HTTP方法 + /// 请求URL + /// 请求体 + /// 请求头,JSON格式 + /// + public async Task Request(string method, string url, string? body = null, string? headersJson = null) + { + _logger.LogDebug($"[HTTP] 发送HTTP请求: {method} {url} Body: {(body != null ? body : "null")} Headers: {(headersJson != null ? headersJson : "null")}"); + CheckHttpPermission(url); + + var dictHeaders = new Dictionary(); + if (!string.IsNullOrWhiteSpace(headersJson)) + { + try + { + var headers = JsonSerializer.Deserialize>(headersJson); + if (headers != null) + { + dictHeaders = headers; + } + } + catch (JsonException) + { + throw new ArgumentException("Headers JSON格式错误"); + } + } + + // header全部小写 + dictHeaders = dictHeaders.ToDictionary(kvp => kvp.Key.ToLowerInvariant(), kvp => kvp.Value); + + // 提前取出来Content-Type,防止被覆盖 + string contentType = "application/json"; + if (dictHeaders.TryGetValue("content-type", out var ct)) + { + contentType = ct; + dictHeaders.Remove("content-type"); + } + + // 使用HttpClient发送请求 + using var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.Clear(); + foreach (var header in dictHeaders) + { + httpClient.DefaultRequestHeaders.Add(header.Key, header.Value); + } + + var content = body == null ? null : new StringContent(body, Encoding.UTF8, contentType); + var response = await httpClient.SendAsync(new HttpRequestMessage(new HttpMethod(method), url) { Content = content }); + + var responseCode = (int)response.StatusCode; + var responseHeaders = response.Headers.ToDictionary(h => h.Key, h => h.Value.First()); // 只取第一个值 + var responseBody = await response.Content.ReadAsStringAsync(); + return new HttpReponse + { + status_code = responseCode, + headers = responseHeaders, + body = responseBody, + }; + } +} diff --git a/BetterGenshinImpact/Core/Script/Dependence/LimitedFile.cs b/BetterGenshinImpact/Core/Script/Dependence/LimitedFile.cs index 733f629d..7a17e681 100644 --- a/BetterGenshinImpact/Core/Script/Dependence/LimitedFile.cs +++ b/BetterGenshinImpact/Core/Script/Dependence/LimitedFile.cs @@ -4,6 +4,8 @@ using System.IO; using System.Threading.Tasks; using OpenCvSharp; using System.Linq; +using BetterGenshinImpact.GameTask.Common; +using Microsoft.Extensions.Logging; namespace BetterGenshinImpact.Core.Script.Dependence; @@ -16,26 +18,35 @@ public class LimitedFile(string rootPath) /// 文件夹内所有文件和文件夹的路径数组 public string[] ReadPathSync(string folderPath) { - // 对传入的文件夹路径进行标准化 - string normalizedFolderPath = NormalizePath(folderPath); - - // 确保目录存在 - if (!Directory.Exists(normalizedFolderPath)) + try { - Directory.CreateDirectory(normalizedFolderPath); + // 对传入的文件夹路径进行标准化 + string normalizedFolderPath = NormalizePath(folderPath); + + // 确保目录存在 + if (!Directory.Exists(normalizedFolderPath)) + { + Directory.CreateDirectory(normalizedFolderPath); + } + + // 获取指定文件夹下的所有文件(非递归) + string[] files = Directory.GetFiles(normalizedFolderPath, "*", SearchOption.TopDirectoryOnly); + + // 获取指定文件夹下的所有子文件夹(非递归) + string[] directories = Directory.GetDirectories(normalizedFolderPath, "*", SearchOption.TopDirectoryOnly); + + // 合并文件和文件夹路径 + string[] combined = files.Concat(directories).ToArray(); + + // 将绝对路径转换为相对于 rootPath 的相对路径 + return combined.Select(path => Path.GetRelativePath(rootPath, path)).ToArray(); + } + catch (Exception ex) + { + // 记录异常并返回空数组 + TaskControl.Logger.LogError("ReadPathSync 异常: {Message}", ex.Message); + return Array.Empty(); } - - // 获取指定文件夹下的所有文件(非递归) - string[] files = Directory.GetFiles(normalizedFolderPath, "*", SearchOption.TopDirectoryOnly); - - // 获取指定文件夹下的所有子文件夹(非递归) - string[] directories = Directory.GetDirectories(normalizedFolderPath, "*", SearchOption.TopDirectoryOnly); - - // 合并文件和文件夹路径 - string[] combined = files.Concat(directories).ToArray(); - - // 将绝对路径转换为相对于 rootPath 的相对路径 - return combined.Select(path => Path.GetRelativePath(rootPath, path)).ToArray(); } /// @@ -45,11 +56,20 @@ public class LimitedFile(string rootPath) /// 如果该路径是文件夹则返回 true,否则返回 false。 public bool IsFolder(string path) { - // 对传入的路径进行标准化处理 - string normalizedPath = NormalizePath(path); + try + { + // 对传入的路径进行标准化处理 + string normalizedPath = NormalizePath(path); - // 使用 Directory.Exists 判断标准化路径是否为文件夹 - return Directory.Exists(normalizedPath); + // 使用 Directory.Exists 判断标准化路径是否为文件夹 + return Directory.Exists(normalizedPath); + } + catch (Exception ex) + { + // 记录异常并返回 false + TaskControl.Logger.LogError("IsFolder 异常: {Message}", ex.Message); + return false; + } } /// @@ -67,8 +87,17 @@ public class LimitedFile(string rootPath) /// Text read from file. public string ReadTextSync(string path) { - path = NormalizePath(path); - return File.ReadAllText(path); + try + { + path = NormalizePath(path); + return File.ReadAllText(path); + } + catch (Exception ex) + { + // 记录异常并返回空字符串 + TaskControl.Logger.LogError("ReadTextSync 异常: {Message}", ex.Message); + return string.Empty; + } } /// @@ -78,9 +107,18 @@ public class LimitedFile(string rootPath) /// Text read from file. public async Task ReadText(string path) { - path = NormalizePath(path); - var ret = await File.ReadAllTextAsync(path); - return ret; + try + { + path = NormalizePath(path); + var ret = await File.ReadAllTextAsync(path); + return ret; + } + catch (Exception ex) + { + // 记录异常并返回空字符串 + TaskControl.Logger.LogError("ReadText 异常: {Message}", ex.Message); + return string.Empty; + } } /// @@ -112,11 +150,70 @@ public class LimitedFile(string rootPath) /// public Mat ReadImageMatSync(string path) { - path = NormalizePath(path); - var mat = Mat.FromStream(File.OpenRead(path), ImreadModes.Color); - return mat; + try + { + path = NormalizePath(path); + using var stream = File.OpenRead(path); + var mat = Mat.FromStream(stream, ImreadModes.Color); + return mat; + } + catch (Exception ex) + { + // 记录异常并返回空的Mat + TaskControl.Logger.LogError("ReadImageMatSync 异常: {Message}", ex.Message); + return new Mat(); + } } + /// + /// 读取图像文件为Mat对象,并调整到指定尺寸 + /// + /// 图像文件路径 + /// 调整后的宽度 + /// 调整后的高度 + /// 插值算法,默认为双线性插值(1) + /// 调整尺寸后的Mat图像对象 + /// + /// 支持的插值算法: + /// + /// 最近邻插值 (0) + /// 双线性插值 (1) - 默认 + /// 双三次插值 (2) + /// 像素区域关系重采样 (3) + /// Lanczos插值 (4) + /// 精确双线性插值 (5) + /// + /// + public Mat ReadImageMatWithResizeSync(string path, double width, double height, int interpolation = 1) + { + try + { + if (width <= 0 || height <= 0) + { + throw new Exception("ReadImageMatWithResizeSync: 宽度和高度必须为正数"); + } + + if (interpolation is < 0 or > 5) + { + throw new Exception($"ReadImageMatWithResizeSync: 不支持的插值算法 {interpolation}"); + } + + path = NormalizePath(path); + using var stream = File.OpenRead(path); + using var mat = Mat.FromStream(stream, ImreadModes.Color); + var rsz = new Mat(); + Cv2.Resize(mat, rsz, new Size(width, height), 0, 0, (InterpolationFlags)interpolation); + return rsz; + } + catch (Exception ex) + { + // 记录异常并返回空的Mat + TaskControl.Logger.LogError("ReadImageMatWithResizeSync 异常: {Message}", ex.Message); + return new Mat(); + } + } + + /// /// 允许的文件扩展名白名单 /// @@ -135,32 +232,41 @@ public class LimitedFile(string rootPath) /// 是否合法 private bool IsValid(string path, string? content = null) { - // 验证文件扩展名 - string extension = Path.GetExtension(path).ToLower(); - if (!Array.Exists(_allowedExtensions, ext => ext == extension)) + try { - return false; - } - - // 确保目录存在 - var normalizedPath = NormalizePath(path); - string? directoryPath = Path.GetDirectoryName(normalizedPath); - if (!string.IsNullOrEmpty(directoryPath) && !Directory.Exists(directoryPath)) - { - Directory.CreateDirectory(directoryPath); - } - - // 如果提供了内容,验证内容是否合法 - if (content != null) - { - // 检查文件大小 - if (content.Length > MaxFileSize) + // 验证文件扩展名 + string extension = Path.GetExtension(path).ToLower(); + if (!Array.Exists(_allowedExtensions, ext => ext == extension)) { return false; } + + // 确保目录存在 + var normalizedPath = NormalizePath(path); + string? directoryPath = Path.GetDirectoryName(normalizedPath); + if (!string.IsNullOrEmpty(directoryPath) && !Directory.Exists(directoryPath)) + { + Directory.CreateDirectory(directoryPath); + } + + // 如果提供了内容,验证内容是否合法 + if (content != null) + { + // 检查文件大小 + if (content.Length > MaxFileSize) + { + return false; + } + } + + return true; + } + catch (Exception ex) + { + // 记录异常并返回 false + TaskControl.Logger.LogError("IsValid 异常: {Message}", ex.Message); + return false; } - - return true; } /// @@ -295,8 +401,10 @@ public class LimitedFile(string rootPath) Cv2.ImWrite(path, mat); return true; } - catch + catch (Exception ex) { + // 记录异常并返回 false + TaskControl.Logger.LogError("WriteImageSync 异常: {Message}", ex.Message); return false; } } diff --git a/BetterGenshinImpact/Core/Script/EngineExtend.cs b/BetterGenshinImpact/Core/Script/EngineExtend.cs index 54b91c57..5b5b4cc4 100644 --- a/BetterGenshinImpact/Core/Script/EngineExtend.cs +++ b/BetterGenshinImpact/Core/Script/EngineExtend.cs @@ -8,6 +8,8 @@ using OpenCvSharp; using BetterGenshinImpact.Core.Recognition; using BetterGenshinImpact.GameTask.Model.Area; using BetterGenshinImpact.Core.Config; +using BetterGenshinImpact.GameTask.AutoDomain; +using BetterGenshinImpact.GameTask.AutoFight; using BetterGenshinImpact.GameTask.AutoFight.Model; namespace BetterGenshinImpact.Core.Script; @@ -30,6 +32,7 @@ public class EngineExtend engine.AddHostObject("genshin", new Dependence.Genshin()); engine.AddHostObject("log", new Log()); engine.AddHostObject("file", new LimitedFile(workDir)); // 限制文件访问 + engine.AddHostObject("http", new Http()); // 限制文件访问 engine.AddHostObject("notification", new Notification()); // 任务调度器 @@ -64,6 +67,9 @@ public class EngineExtend engine.AddHostType("ServerTime", typeof(ServerTime)); + engine.AddHostType("AutoDomainParam", typeof(AutoDomainParam)); + engine.AddHostType("AutoFightParam", typeof(AutoFightParam)); + // 添加C#的类型 @@ -97,6 +103,7 @@ public class EngineExtend engine.AddHostObject("keyUp", GlobalMethod.KeyUp); engine.AddHostObject("keyPress", GlobalMethod.KeyPress); engine.AddHostObject("setGameMetrics", GlobalMethod.SetGameMetrics); + engine.AddHostObject("getGameMetrics", GlobalMethod.GetGameMetrics); engine.AddHostObject("moveMouseBy", GlobalMethod.MoveMouseBy); engine.AddHostObject("moveMouseTo", GlobalMethod.MoveMouseTo); engine.AddHostObject("click", GlobalMethod.Click); diff --git a/BetterGenshinImpact/Core/Script/Group/ScriptGroupProject.cs b/BetterGenshinImpact/Core/Script/Group/ScriptGroupProject.cs index 47a05cea..48a6b715 100644 --- a/BetterGenshinImpact/Core/Script/Group/ScriptGroupProject.cs +++ b/BetterGenshinImpact/Core/Script/Group/ScriptGroupProject.cs @@ -106,6 +106,22 @@ public partial class ScriptGroupProject : ObservableObject [ObservableProperty] private bool? _allowJsNotification = true; + [ObservableProperty] + private string? _allowJsHTTPHash = ""; + + /// + /// 是否允许JS脚本发送HTTP请求,通过验证Hash来控制 + /// + [JsonIgnore] + public bool AllowJsHTTP + { + get + { + return GetHttpAllowedUrlsHash() == AllowJsHTTPHash; + } + } + + public ScriptGroupProject() { } @@ -163,6 +179,19 @@ public partial class ScriptGroupProject : ObservableObject Project = new ScriptProject(FolderName); } + public string GetHttpAllowedUrlsHash() + { + if (Project == null) + { + BuildScriptProjectRelation(); + } + if (Project == null) + { + return ""; + } + return string.Join("|", Project.Manifest.HttpAllowedUrls); + } + public async Task Run() { //执行记录 diff --git a/BetterGenshinImpact/Core/Script/Project/Manifest.cs b/BetterGenshinImpact/Core/Script/Project/Manifest.cs index 0aa31798..d732a711 100644 --- a/BetterGenshinImpact/Core/Script/Project/Manifest.cs +++ b/BetterGenshinImpact/Core/Script/Project/Manifest.cs @@ -26,6 +26,7 @@ public class Manifest public string[] Scripts { get; set; } = []; public string[] Library { get; set; } = []; public string[] SavedFiles { get; set; } = []; + public string[] HttpAllowedUrls { get; set; } = []; public static Manifest FromJson(string json) { diff --git a/BetterGenshinImpact/Core/Script/ScriptRepoUpdater.cs b/BetterGenshinImpact/Core/Script/ScriptRepoUpdater.cs index baef9a99..35cc8578 100644 --- a/BetterGenshinImpact/Core/Script/ScriptRepoUpdater.cs +++ b/BetterGenshinImpact/Core/Script/ScriptRepoUpdater.cs @@ -4,6 +4,8 @@ using BetterGenshinImpact.Core.Script.WebView; using BetterGenshinImpact.GameTask; using BetterGenshinImpact.Helpers; using BetterGenshinImpact.Helpers.Http; +using BetterGenshinImpact.Helpers.Ui; +using BetterGenshinImpact.Helpers.Win32; using BetterGenshinImpact.Model; using BetterGenshinImpact.View.Controls.Webview; using BetterGenshinImpact.ViewModel.Pages; @@ -172,7 +174,8 @@ public class ScriptRepoUpdater : Singleton var fetchOptions = new FetchOptions { ProxyOptions = { ProxyType = ProxyType.None }, - Depth = 1 // 浅拉取,只获取最新的提交 + Depth = 1, // 浅拉取,只获取最新的提交 + CredentialsProvider = CreateCredentialsHandler() // 添加凭据处理器 }; Commands.Fetch(repo, remote.Name, refSpecs, fetchOptions, "拉取最新更新"); @@ -417,6 +420,8 @@ public class ScriptRepoUpdater : Singleton OnCheckoutProgress = onCheckoutProgress }; // options.FetchOptions.Depth = 1; // 浅克隆,只获取最新的提交 + // 设置凭据处理器 + options.FetchOptions.CredentialsProvider = Instance.CreateCredentialsHandler(); // 克隆仓库 Repository.Clone(repoUrl, repoPath, options); } @@ -451,7 +456,8 @@ public class ScriptRepoUpdater : Singleton { TagFetchMode = TagFetchMode.None, ProxyOptions = { ProxyType = ProxyType.None }, - Depth = 1 // 浅拉取,只获取最新的提交 + Depth = 1, // 浅拉取,只获取最新的提交 + CredentialsProvider = CreateCredentialsHandler() // 添加凭据处理器 }; string refSpec = $"+refs/heads/{branchName}:refs/remotes/origin/{branchName}"; Commands.Fetch(repo, remote.Name, new[] { refSpec }, fetchOptions, "初始化拉取"); @@ -488,6 +494,28 @@ public class ScriptRepoUpdater : Singleton repo.Config.Unset("https.proxy"); } + /// + /// 创建凭据处理器(用于私有仓库身份验证) + /// + /// 凭据处理器 + private CredentialsHandler? CreateCredentialsHandler() + { + // 从凭据管理器读取 Git 凭据 + var credential = CredentialManagerHelper.ReadCredential("BetterGenshinImpact.GitCredentials"); + + + // 返回凭据处理器 + return (url, usernameFromUrl, types) => + { + _logger.LogInformation($"使用配置的Git凭据进行身份验证"); + return new UsernamePasswordCredentials + { + Username = credential?.UserName ?? "", + Password = credential?.Password ?? "" + }; + }; + } + // [Obsolete] // public async Task<(string, bool)> UpdateCenterRepo() // { @@ -656,6 +684,7 @@ public class ScriptRepoUpdater : Singleton Owner = Application.Current.MainWindow, WindowStartupLocation = WindowStartupLocation.CenterOwner, }; + uiMessageBox.SourceInitialized += (s, e) => WindowHelper.TryApplySystemBackdrop(uiMessageBox); var result = await uiMessageBox.ShowDialogAsync(); if (result == Wpf.Ui.Controls.MessageBoxResult.Primary) @@ -730,7 +759,7 @@ public class ScriptRepoUpdater : Singleton } catch { - await MessageBox.ErrorAsync("本地无仓库信息,请至少成功更新一次脚本仓库信息!"); + await ThemedMessageBox.ErrorAsync("本地无仓库信息,请至少成功更新一次脚本仓库信息!"); return; } @@ -1060,13 +1089,67 @@ public class ScriptRepoUpdater : Singleton UpdateSubscribedScriptPaths(); if (_webWindow is not { IsVisible: true }) { + var scriptConfig = TaskContext.Instance().Config.ScriptConfig; + + // 计算宽高(默认0.7屏幕宽高) + double width = scriptConfig.WebviewWidth == 0 + ? SystemParameters.WorkArea.Width * 0.7 + : scriptConfig.WebviewWidth; + + double height = scriptConfig.WebviewHeight == 0 + ? SystemParameters.WorkArea.Height * 0.7 + : scriptConfig.WebviewHeight; + + // 计算位置(默认居中) + double left = scriptConfig.WebviewLeft == 0 + ? (SystemParameters.WorkArea.Width - width) / 2 + : scriptConfig.WebviewLeft; + + double top = scriptConfig.WebviewTop == 0 + ? (SystemParameters.WorkArea.Height - height) / 2 + : scriptConfig.WebviewTop; + + WindowState state = scriptConfig.WebviewState; + var screen = SystemParameters.WorkArea; + bool isSmallScreen = screen.Width <= 1600 || screen.Height <= 900; + // 如果未设置或非法值,则默认 Normal,小屏则直接最大化 + if (isSmallScreen) + { + state = WindowState.Maximized; + } + else if (!Enum.IsDefined(typeof(WindowState), scriptConfig.WebviewState)) + { + state = WindowState.Normal; + } + else + { + state = scriptConfig.WebviewState; + } + _webWindow = new WebpageWindow { Title = "Genshin Copilot Scripts | BetterGI 脚本本地中央仓库", - Width = 1366, - Height = 768, + Width = width, + Height = height, + Left = left, + Top = top, + WindowStartupLocation = WindowStartupLocation.Manual, + WindowState = state + }; + // 关闭时保存窗口位置与大小 + _webWindow.Closed += (s, e) => + { + if (_webWindow != null) + { + scriptConfig.WebviewLeft = _webWindow.Left; + scriptConfig.WebviewTop = _webWindow.Top; + scriptConfig.WebviewWidth = _webWindow.Width; + scriptConfig.WebviewHeight = _webWindow.Height; + scriptConfig.WebviewState = _webWindow.WindowState; + } + + _webWindow = null; }; - _webWindow.Closed += (s, e) => _webWindow = null; _webWindow.Panel!.DownloadFolderPath = MapPathingViewModel.PathJsonPath; _webWindow.NavigateToFile(Global.Absolute(@"Assets\Web\ScriptRepo\index.html")); _webWindow.Panel!.OnWebViewInitializedAction = () => diff --git a/BetterGenshinImpact/Core/Script/Utils/ScriptUtils.cs b/BetterGenshinImpact/Core/Script/Utils/ScriptUtils.cs index b5a05d01..abd5c0ea 100644 --- a/BetterGenshinImpact/Core/Script/Utils/ScriptUtils.cs +++ b/BetterGenshinImpact/Core/Script/Utils/ScriptUtils.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Linq; namespace BetterGenshinImpact.Core.Script.Utils; @@ -10,17 +11,30 @@ public class ScriptUtils /// public static string NormalizePath(string root, string path) { - // convert to full path relative to root - path = path.Replace('\\', '/'); - var fullPath = Path.GetFullPath(Path.Combine(root, path)); + // 校验空字符串 + if (string.IsNullOrWhiteSpace(path)) + throw new ArgumentException("文件路径不能为空"); - // if root is locked, make sure didn't attempt to exit it - if (!fullPath.StartsWith(root)) + // 检查是否含有非法文件名字符 + var invalidChars = Path.GetInvalidFileNameChars(); + string fileName = Path.GetFileName(path); + if (fileName.Any(c => invalidChars.Contains(c))) { - throw new ArgumentException($"Path '{path}' is not allowed, because its outside the caged root folder!"); + throw new ArgumentException($"文件路径 '{path}' 包含非法字符"); + } + + // 替换分隔符 + path = path.Replace('\\', '/'); + + // 组合并获取绝对路径 + var fullPath = Path.GetFullPath(Path.Combine(root, path)); + + // 防止越界访问 + if (!fullPath.StartsWith(root, StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException($"文件路径 '{path}' 越界访问!"); } - // return full path return fullPath; } } diff --git a/BetterGenshinImpact/Core/Script/WebView/RepoWebBridge.cs b/BetterGenshinImpact/Core/Script/WebView/RepoWebBridge.cs index 0fd1459b..c6573843 100644 --- a/BetterGenshinImpact/Core/Script/WebView/RepoWebBridge.cs +++ b/BetterGenshinImpact/Core/Script/WebView/RepoWebBridge.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Threading.Tasks; +using BetterGenshinImpact.View.Windows; using BetterGenshinImpact.ViewModel.Message; using CommunityToolkit.Mvvm.Messaging; using Newtonsoft.Json.Linq; @@ -39,7 +40,7 @@ public sealed class RepoWebBridge } catch (Exception ex) { - await MessageBox.ShowAsync(ex.Message, "获取仓库信息失败"); + await ThemedMessageBox.ErrorAsync(ex.Message, "获取仓库信息失败"); return string.Empty; } } @@ -53,7 +54,7 @@ public sealed class RepoWebBridge } catch (Exception e) { - await MessageBox.ShowAsync(e.Message, "订阅脚本链接失败!"); + await ThemedMessageBox.ErrorAsync(e.Message, "订阅脚本链接失败!"); } } @@ -63,7 +64,7 @@ public sealed class RepoWebBridge if (!File.Exists(userConfigPath)) { - await MessageBox.ShowAsync($"用户配置文件不存在: {userConfigPath}", "获取用户配置失败"); + await ThemedMessageBox.ErrorAsync($"用户配置文件不存在: {userConfigPath}", "获取用户配置失败"); return string.Empty; } @@ -118,7 +119,7 @@ public sealed class RepoWebBridge } catch (Exception ex) { - await MessageBox.ShowAsync(ex.Message, "信息更新失败"); + await ThemedMessageBox.ErrorAsync(ex.Message, "信息更新失败"); return false; } } @@ -144,7 +145,7 @@ public sealed class RepoWebBridge } catch (Exception ex) { - await MessageBox.ShowAsync($"清空更新标记失败: {ex.Message}", "操作失败"); + await ThemedMessageBox.ErrorAsync($"清空更新标记失败: {ex.Message}", "操作失败"); return false; } } diff --git a/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageConfig.cs b/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageConfig.cs index 5762ca55..62c083b5 100644 --- a/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageConfig.cs +++ b/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageConfig.cs @@ -8,11 +8,10 @@ public partial class AutoArtifactSalvageConfig : ObservableObject { // JavaScript [ObservableProperty] - private string _javaScript = @"(async function (artifact) { - var hasATK = Array.from(artifact.MinorAffixes).some(affix => affix.Type == 'ATK'); - var hasDEF = Array.from(artifact.MinorAffixes).some(affix => affix.Type == 'DEF'); - Output = hasATK && hasDEF; -})(ArtifactStat);"; + private string _javaScript = @"var hasATK = Array.from(ArtifactStat.MinorAffixes).some(affix => affix.Type == 'ATK'); +var hasDEF = Array.from(ArtifactStat.MinorAffixes).some(affix => affix.Type == 'DEF'); +var hasHP = Array.from(ArtifactStat.MinorAffixes).some(affix => affix.Type == 'HP'); +Output = (hasATK && hasDEF) || (hasHP && hasDEF);"; // JavaScript [ObservableProperty] @@ -32,7 +31,7 @@ public partial class AutoArtifactSalvageConfig : ObservableObject [ObservableProperty] private int _maxNumToCheck = 100; - // 单次识别失败政策 + // 单次识别失败策略 [ObservableProperty] private RecognitionFailurePolicy _recognitionFailurePolicy = RecognitionFailurePolicy.Skip; } \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageTask.cs b/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageTask.cs index 7c61a7ea..09f8c3a7 100644 --- a/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageTask.cs +++ b/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageTask.cs @@ -12,6 +12,7 @@ using BetterGenshinImpact.GameTask.Model.Area; using BetterGenshinImpact.GameTask.Model.GameUI; using BetterGenshinImpact.Helpers; using BetterGenshinImpact.Helpers.Extensions; +using BetterGenshinImpact.View.Drawable; using Fischless.WindowsInput; using Microsoft.ClearScript; using Microsoft.ClearScript.V8; @@ -303,21 +304,49 @@ public class AutoArtifactSalvageTask : ISoloTask quickSelectConfirmBtn.Click(); await Delay(400, ct); // 点击所属套装 - ra5.ClickTo(315, 205); + ra5.ClickTo(315, 190); await Delay(1000, ct); // 遍历套装Grid勾选套装 using InferenceSession session = GridIconsAccuracyTestTask.LoadModel(out Dictionary prototypes); ArtifactSetFilterScreen gridScreen = new ArtifactSetFilterScreen(new GridParams(new Rect(40, 100, 1300, 852), 2, 3, 40, 40, 0.024), this.logger, this.ct); - await foreach (ImageRegion itemRegion in gridScreen) + string drawKey = "ArtifactSetFilter"; + var drawRectList = new List(); + var drawTextList = new List(); + gridScreen.OnBeforeScroll += () => { VisionContext.Instance().DrawContent.RemoveRect(drawKey); drawRectList.Clear(); drawTextList.Clear(); }; + try { - using Mat img125 = GetGridIconsTask.CropResizeArtifactSetFilterGridIcon(itemRegion); - (string predName, _) = GridIconsAccuracyTestTask.Infer(img125, session, prototypes); - if (this.artifactSetFilter.Contains(predName)) + await foreach ((ImageRegion pageRegion, Rect itemRect) in gridScreen) { - itemRegion.Click(); - await Delay(100, ct); + using ImageRegion itemRegion = pageRegion.DeriveCrop(itemRect); + using Mat img125 = GetGridIconsTask.CropResizeArtifactSetFilterGridIcon(itemRegion); + (string? predName, _) = GridIconsAccuracyTestTask.Infer(img125, session, prototypes); + if (predName == null) + { + var rectDrawable = itemRegion.SelfToRectDrawable(drawKey); + drawRectList.Add(rectDrawable); + VisionContext.Instance().DrawContent.PutOrRemoveRectList(drawKey, drawRectList); + drawTextList.Add(new TextDrawable("识别失败", new System.Windows.Point(rectDrawable.Rect.X + rectDrawable.Rect.Width / 3, rectDrawable.Rect.Y))); + VisionContext.Instance().DrawContent.TextList.GetOrAdd(drawKey, drawTextList); + } + else + { + var rectDrawable = itemRegion.SelfToRectDrawable(drawKey, System.Drawing.Pens.Lime); + drawRectList.Add(rectDrawable); + VisionContext.Instance().DrawContent.PutOrRemoveRectList(drawKey, drawRectList); + drawTextList.Add(new TextDrawable(predName, new System.Windows.Point(rectDrawable.Rect.X + rectDrawable.Rect.Width / 3, rectDrawable.Rect.Y))); + VisionContext.Instance().DrawContent.TextList.GetOrAdd(drawKey, drawTextList); + if (this.artifactSetFilter.Contains(predName)) + { + itemRegion.Click(); + await Delay(100, ct); + } + } } } + finally + { + VisionContext.Instance().DrawContent.ClearAll(); + } // 点击确认筛选 using var confirmFilterBtnRegion = CaptureToRectArea(); Bv.ClickWhiteConfirmButton(confirmFilterBtnRegion); @@ -351,60 +380,70 @@ public class AutoArtifactSalvageTask : ISoloTask GridParams gridParams = GridParams.Templates[GridScreenName.ArtifactSalvage]; GridScreen gridScreen = new GridScreen(gridParams, this.logger, this.ct); // 圣遗物分解Grid有4行9列 - await foreach (ImageRegion itemRegion in gridScreen) + gridScreen.OnAfterTurnToNewPage += GridScreen.DrawItemsAfterTurnToNewPage; + gridScreen.OnBeforeScroll += () => VisionContext.Instance().DrawContent.ClearAll(); + try { - Rect gridRect = itemRegion.ToRect(); - if (GetArtifactStatus(itemRegion.SrcMat) == ArtifactStatus.None) + await foreach ((ImageRegion pageRegion, Rect itemRect) in gridScreen) { - itemRegion.Click(); - await Delay(300, ct); - - using var ra1 = CaptureToRectArea(); - using ImageRegion itemRegion1 = ra1.DeriveCrop(gridRect + new Point(gridParams.Roi.X, gridParams.Roi.Y)); - if (GetArtifactStatus(itemRegion1.SrcMat) == ArtifactStatus.Selected) + using ImageRegion itemRegion = pageRegion.DeriveCrop(itemRect); + Rect gridRect = itemRegion.ToRect(); + if (GetArtifactStatus(itemRegion.SrcMat) == ArtifactStatus.None) { - 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))); + itemRegion.Click(); + await Delay(300, ct); - ArtifactStat artifact; - try + using var ra1 = CaptureToRectArea(); + using ImageRegion itemRegion1 = ra1.DeriveCrop(gridRect + new Point(gridParams.Roi.X, gridParams.Roi.Y)); + if (GetArtifactStatus(itemRegion1.SrcMat) == ArtifactStatus.Selected) { - artifact = GetArtifactStat(card.SrcMat, OcrFactory.Paddle, out string allText); - } - catch (Exception e) - { - if (recognitionFailurePolicy == RecognitionFailurePolicy.Skip) + 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 { - logger.LogError("识别失败,跳过当前圣遗物:{msg}", e.Message); + 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; + itemRegion.Click(); // 反选取消 + await Delay(100, ct); + continue; + } + else + { + throw; + } + } + + if (await IsMatchJavaScript(artifact, javaScript)) + { + // logger.LogInformation(message: msg); } else { - throw; + itemRegion.Click(); // 反选取消 + await Delay(100, ct); } } - if (IsMatchJavaScript(artifact, javaScript)) + count--; + if (count <= 0) { - // logger.LogInformation(message: msg); + logger.LogInformation("检查次数已耗尽"); + break; } - else - { - itemRegion.Click(); // 反选取消 - await Delay(100, ct); - } - } - - count--; - if (count <= 0) - { - logger.LogInformation("检查次数已耗尽"); - break; } } } + finally + { + VisionContext.Instance().DrawContent.ClearAll(); + } } /// @@ -412,20 +451,33 @@ public class AutoArtifactSalvageTask : ISoloTask /// /// 作为JS入参,JS使用“ArtifactStat”获取 /// - /// 由调用者控制生命周期 + /// 为空则默认创建一个3秒延迟的cts /// 是否匹配。取JS的“Output”作为出参 /// /// - public static bool IsMatchJavaScript(ArtifactStat artifact, string javaScript) + public async static Task IsMatchJavaScript(ArtifactStat artifact, string javaScript, ILogger? logger = null, TimeProvider? timeProvider = null) { + logger = logger ?? App.GetLogger(); using V8ScriptEngine engine = new V8ScriptEngine(V8ScriptEngineFlags.UseCaseInsensitiveMemberBinding | V8ScriptEngineFlags.DisableGlobalMembers); + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3), timeProvider ?? TimeProvider.System); // 这里只是用JS写一个自定义判断方法,由于每个圣遗物都会执行一次,这个方法不应执行太久 + cts.Token.Register(() => + { + try + { + engine.Interrupt(); + } + catch (Exception ex) + { + Console.WriteLine($"中断失败: {ex.Message}"); + } + }); try { // 传入输入参数 engine.Script.ArtifactStat = artifact; // 执行JavaScript代码 - engine.Execute(javaScript); + await Task.Run(() => engine.Execute(javaScript)); // 检查是否有输出 if (!engine.Script.propertyIsEnumerable("Output")) @@ -440,6 +492,11 @@ public class AutoArtifactSalvageTask : ISoloTask return (bool)engine.Script.Output; } + catch (ScriptInterruptedException) + { + logger.LogWarning("脚本执行超出3秒限制,请使用正确的JS代码(JavaScript execution timeout!)"); + throw; + } catch (ScriptEngineException ex) { throw new Exception($"JavaScript execution error: {ex.Message}", ex); @@ -471,25 +528,25 @@ public class AutoArtifactSalvageTask : ISoloTask public ArtifactStat GetArtifactStat(Mat src, IOcrService ocrService, out string allText) { using Mat gray = src.CvtColor(ColorConversionCodes.BGR2GRAY); - Mat hatKernel = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(15, 15)/*需根据实际文本大小调整*/); // 顶帽运算核 + using 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))); + using 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))); + using 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 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(); + using Mat levelAndMinorAffixRoi = gray.SubMat(new Rect(0, (int)(src.Height * 0.52), src.Width, (int)(src.Height * 0.48))); + //using Mat levelAndMinorAffixRoiThreshold = new Mat(); // otsu确定阈值大概在170 //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(); + //using Mat levelAndMinorAffixRoiThreshold = levelAndMinorAffixRoi.Threshold(170, 255, ThresholdTypes.Binary); + #endregion var nameOcrResult = ocrService.OcrResult(nameRoi); var typeOcrResult = ocrService.OcrResult(typeRoi); @@ -552,7 +609,7 @@ public class AutoArtifactSalvageTask : ISoloTask #region 副词条 var minorAffixes = new List(); - string pattern = @"^([^+::]+)\+([\d., ]*)(%?).*$"; + string pattern = @"^([^+::]+)\+([\d., 。]*)(%?).*$"; pattern = pattern.Replace("%", percentStr); foreach (var r in levelAndMinorAffixResult) { @@ -560,7 +617,7 @@ public class AutoArtifactSalvageTask : ISoloTask if (!match.Success) { continue; - } + } ArtifactAffixType artifactAffixType; var dic = this.artifactAffixStrDic; if (match.Groups[1].Value.Contains(dic[ArtifactAffixType.ATK])) @@ -617,11 +674,11 @@ public class AutoArtifactSalvageTask : ISoloTask throw new Exception($"未识别的副词条:{match.Groups[1].Value}"); } - if (!float.TryParse(match.Groups[2].Value, NumberStyles.Any, cultureInfo, out float affixValue)) + if (!float.TryParse(match.Groups[2].Value.Replace("。", "."), NumberStyles.Any, cultureInfo, out float affixValue)) { throw new Exception($"未识别的副词条数值:{match.Groups[2].Value}"); } - + bool isUnactivated = false; // 只有在已经成功识别至少 3 个词条后才执行额外的直方图分析。 if (minorAffixes.Count >= 3) diff --git a/BetterGenshinImpact/GameTask/AutoDomain/AutoDomainParam.cs b/BetterGenshinImpact/GameTask/AutoDomain/AutoDomainParam.cs index 89020538..08167c9e 100644 --- a/BetterGenshinImpact/GameTask/AutoDomain/AutoDomainParam.cs +++ b/BetterGenshinImpact/GameTask/AutoDomain/AutoDomainParam.cs @@ -1,10 +1,10 @@ using System.Collections.Generic; using BetterGenshinImpact.GameTask.Model; -using System.Threading; +using BetterGenshinImpact.Core.Config; namespace BetterGenshinImpact.GameTask.AutoDomain; -public class AutoDomainParam : BaseTaskParam +public class AutoDomainParam : BaseTaskParam { public int DomainRoundNum { get; set; } @@ -74,4 +74,41 @@ public class AutoDomainParam : BaseTaskParam FragileResinUseCount = config.FragileResinUseCount; SpecifyResinUse = config.SpecifyResinUse; } + + public AutoDomainParam(int domainRoundNum = 0) : base(null, null) + { + DomainRoundNum = domainRoundNum; + if (domainRoundNum == 0) + { + DomainRoundNum = 9999; + } + + CombatStrategyPath = SetCombatStrategyPath(); + SetDefault(); + } + + /// + /// 设置战斗策略路径 + /// + /// 策略名称 + public string SetCombatStrategyPath(string? strategyName = null) + { + if (string.IsNullOrEmpty(strategyName)) + { + strategyName = TaskContext.Instance().Config.AutoFightConfig.StrategyName; + } + + if ("根据队伍自动选择".Equals(strategyName)) + { + return Global.Absolute(@"User\AutoFight\"); + } + + return Global.Absolute(@"User\AutoFight\" + strategyName + ".txt"); + } + + public void SetResinPriorityList(params string[] priorities) + { + ResinPriorityList.Clear(); + ResinPriorityList.AddRange(priorities); + } } \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/AutoDomain/AutoDomainTask.cs b/BetterGenshinImpact/GameTask/AutoDomain/AutoDomainTask.cs index f18e35f0..ddf9bfd6 100644 --- a/BetterGenshinImpact/GameTask/AutoDomain/AutoDomainTask.cs +++ b/BetterGenshinImpact/GameTask/AutoDomain/AutoDomainTask.cs @@ -176,7 +176,8 @@ public class AutoDomainTask : ISoloTask { combatScenes = new CombatScenes().InitializeTeam(CaptureToRectArea()); } - RetryTeamInit(combatScenes);// 队伍没初始化成功则重试 + + RetryTeamInit(combatScenes); // 队伍没初始化成功则重试 // 0. 切换到第一个角色 var combatCommands = FindCombatScriptAndSwitchAvatar(combatScenes); @@ -274,12 +275,12 @@ public class AutoDomainTask : ISoloTask if ("芬德尼尔之顶".Equals(_taskParam.DomainName)) { menuFound = await NewRetry.WaitForElementAppear( - AutoPickAssets.Instance.PickRo, - () => Simulation.SendInput.SimulateAction(GIActions.MoveBackward, KeyType.KeyDown), - _ct, - 20, - 500 - ); + AutoPickAssets.Instance.PickRo, + () => Simulation.SendInput.SimulateAction(GIActions.MoveBackward, KeyType.KeyDown), + _ct, + 20, + 500 + ); Simulation.SendInput.SimulateAction(GIActions.MoveBackward, KeyType.KeyUp); } else if ("无妄引咎密宫".Equals(_taskParam.DomainName)) @@ -296,14 +297,13 @@ public class AutoDomainTask : ISoloTask 500 ); Simulation.SendInput.SimulateAction(GIActions.MoveLeft, KeyType.KeyUp); - } else if ("太山府".Equals(_taskParam.DomainName)) { menuFound = await NewRetry.WaitForElementAppear( AutoPickAssets.Instance.PickRo, () => { }, - _ct, + _ct, 20, 500 ); @@ -311,11 +311,11 @@ public class AutoDomainTask : ISoloTask else { menuFound = await NewRetry.WaitForElementAppear( - AutoPickAssets.Instance.PickRo, - () => Simulation.SendInput.SimulateAction(GIActions.MoveForward, KeyType.KeyDown), - _ct, - 20, - 500 + AutoPickAssets.Instance.PickRo, + () => Simulation.SendInput.SimulateAction(GIActions.MoveForward, KeyType.KeyDown), + _ct, + 20, + 500 ); Simulation.SendInput.SimulateAction(GIActions.MoveForward, KeyType.KeyUp); } @@ -336,7 +336,6 @@ public class AutoDomainTask : ISoloTask { throw new Exception("请检查是否已进入秘境页面"); } - } else { @@ -467,6 +466,7 @@ public class AutoDomainTask : ISoloTask ra2.Dispose(); Logger.LogInformation("自动秘境:点击 {Text}", "单人挑战"); } + using var confirmRectArea2 = ra.Find(RecognitionObject.Ocr(ra.Width * 0.263, ra.Height * 0.32, ra.Width - ra.Width * 0.263 * 2, ra.Height - ra.Height * 0.32 - ra.Height * 0.353)); if (confirmRectArea2.IsExist() && confirmRectArea2.Text.Contains("是否仍要挑战该秘境")) @@ -483,10 +483,7 @@ public class AutoDomainTask : ISoloTask // 等待队伍选择界面出现 var teamUiFound = await NewRetry.WaitForElementAppear( ElementAssets.Instance.PartyBtnChooseView, - () => - { - Logger.LogInformation("自动秘境:进入 {Text}", "队伍选择界面"); - }, + () => { Logger.LogInformation("自动秘境:进入 {Text}", "队伍选择界面"); }, _ct, 10, 1000 @@ -520,6 +517,7 @@ public class AutoDomainTask : ISoloTask { Logger.LogWarning("开始挑战按钮未出现或未能点击。"); } + // 载入 await Delay(1000, _ct); } @@ -556,6 +554,7 @@ public class AutoDomainTask : ISoloTask done.Dispose(); Logger.LogInformation("自动秘境:点击 {Text}", done.Text); } + // 检查左下角区域是否还存在目标文字,消失则继续,存在则结束 using var leftBottom = CaptureToRectArea(); var leftBottomOcr = leftBottom.Find(AutoFightAssets.Instance.AbnormalIconRa); @@ -1120,13 +1119,13 @@ public class AutoDomainTask : ISoloTask bool resinUsed = false; if (resinStatus.CondensedResinCount > 0) { - resinUsed = PressUseResin(ra3, "浓缩树脂"); + (resinUsed, _) = PressUseResin(ra3, "浓缩树脂"); resinStatus.CondensedResinCount -= 1; } else if (resinStatus.OriginalResinCount >= 20) { - resinUsed = PressUseResin(ra3, "原粹树脂"); - resinStatus.OriginalResinCount -= 20; + (resinUsed, var num) = PressUseResin(ra3, "原粹树脂"); + resinStatus.OriginalResinCount -= num; } if (!resinUsed) @@ -1147,18 +1146,19 @@ public class AutoDomainTask : ISoloTask // 指定使用树脂 var textListInPrompt2 = ra3.FindMulti(RecognitionObject.Ocr(ra3.Width * 0.25, ra3.Height * 0.2, ra3.Width * 0.5, ra3.Height * 0.6)); // 按优先级使用 - var failCount = 0; + int successCount = 0; foreach (var record in _resinPriorityListWhenSpecifyUse) { - if (record.RemainCount > 0 && PressUseResin(textListInPrompt2, record.Name)) + if (record.RemainCount > 0) { - record.RemainCount -= 1; - Logger.LogInformation("自动秘境:{Name} 刷取 {Re}/{Max}", record.Name, record.MaxCount - record.RemainCount, record.MaxCount); - break; - } - else - { - failCount++; + var (success, _) = PressUseResin(textListInPrompt2, record.Name); + if (success) + { + record.RemainCount -= 1; + Logger.LogInformation("自动秘境:{Name} 刷取 {Re}/{Max}", record.Name, record.MaxCount - record.RemainCount, record.MaxCount); + successCount++; + break; + } } } @@ -1168,7 +1168,7 @@ public class AutoDomainTask : ISoloTask isLastTurn = true; } - if (failCount == _resinPriorityListWhenSpecifyUse.Count) + if (successCount == 0) { // 没有找到对应的树脂 Logger.LogWarning("自动秘境:指定树脂领取次数时,当前可用树脂选项无法满足配置。你可能设置的刷取次数过多!退出秘境。"); @@ -1230,6 +1230,7 @@ public class AutoDomainTask : ISoloTask } } } + return true; } } @@ -1249,13 +1250,13 @@ public class AutoDomainTask : ISoloTask Bv.ClickBlackConfirmButton(CaptureToRectArea()); } - private bool PressUseResin(ImageRegion ra, string resinName) + public static (bool, int) PressUseResin(ImageRegion ra, string resinName) { var regionList = ra.FindMulti(RecognitionObject.Ocr(ra.Width * 0.25, ra.Height * 0.2, ra.Width * 0.5, ra.Height * 0.6)); return PressUseResin(regionList, resinName); } - private bool PressUseResin(List regionList, string resinName) + public static (bool, int) PressUseResin(List regionList, string resinName) { var resinKey = regionList.FirstOrDefault(t => t.Text.Contains(resinName)); if (resinKey != null) @@ -1272,10 +1273,11 @@ public class AutoDomainTask : ISoloTask // 点击使用 useKey.Click(); // 解决水龙王按下左键后没松开,然后后续点击按下就没反应了。使用双击 - Sleep(60, _ct); + Sleep(60); useKey.Click(); - Logger.LogInformation("自动秘境:使用 {ResinName}", resinName); - return true; + var num = GetResinNum(resinKey, resinName); + Logger.LogInformation("自动秘境:使用 {ResinName}, 数量:{Num}", resinName, num); + return (true, num); } else { @@ -1288,13 +1290,41 @@ public class AutoDomainTask : ISoloTask } } - return false; + return (false, 0); + } + + private static int GetResinNum(Region region, string resinName) + { + if (resinName == "原粹树脂") + { + if (region.Text.Contains("20")) + { + return 20; + } + else if (region.Text.Contains("40")) + { + return 40; + } + else + { + Logger.LogWarning("自动秘境:未识别到原粹树脂消耗体力数量,默认按20计算"); + return 20; + } + } + else if (resinName == "浓缩树脂" || resinName == "脆弱树脂" || resinName == "须臾树脂") + { + return 1; + } + else + { + throw new ArgumentException("未知的树脂名称"); + } } /// /// 判断两个区域在垂直方向上是否有重叠 /// - private bool IsHeightOverlap(Region region1, Region region2) + private static bool IsHeightOverlap(Region region1, Region region2) { int region1Top = region1.Y; int region1Bottom = region1.Y + region1.Height; @@ -1305,7 +1335,7 @@ public class AutoDomainTask : ISoloTask return (region1Top <= region2Bottom && region1Bottom >= region2Top); } - private async Task ArtifactSalvage() + private async Task ArtifactSalvage() { if (!_taskParam.AutoArtifactSalvage) { @@ -1319,4 +1349,4 @@ public class AutoDomainTask : ISoloTask await new AutoArtifactSalvageTask(new AutoArtifactSalvageTaskParam(star, javaScript: null, artifactSetFilter: null, maxNumToCheck: null, recognitionFailurePolicy: null)).Start(_ct); } -} +} \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/AutoEat/AutoEatTask.cs b/BetterGenshinImpact/GameTask/AutoEat/AutoEatTask.cs index b584a34e..2ff472f4 100644 --- a/BetterGenshinImpact/GameTask/AutoEat/AutoEatTask.cs +++ b/BetterGenshinImpact/GameTask/AutoEat/AutoEatTask.cs @@ -9,6 +9,7 @@ using BetterGenshinImpact.GameTask.GetGridIcons; using BetterGenshinImpact.GameTask.Model; using BetterGenshinImpact.GameTask.Model.Area; using BetterGenshinImpact.GameTask.Model.GameUI; +using BetterGenshinImpact.View.Drawable; using Fischless.WindowsInput; using Microsoft.Extensions.Logging; using Microsoft.ML.OnnxRuntime; @@ -86,42 +87,52 @@ public class AutoEatTask : BaseIndependentTask, ISoloTask using InferenceSession session = GridIconsAccuracyTestTask.LoadModel(out Dictionary prototypes); GridScreen gridScreen = new GridScreen(GridParams.Templates[GridScreenName.Food], _logger, _ct); + gridScreen.OnAfterTurnToNewPage += GridScreen.DrawItemsAfterTurnToNewPage; + gridScreen.OnBeforeScroll += () => VisionContext.Instance().DrawContent.ClearAll(); int? count = null; - await foreach (ImageRegion itemRegion in gridScreen) + try { - using Mat icon = itemRegion.SrcMat.GetGridIcon(); - var result = GridIconsAccuracyTestTask.Infer(icon, session, prototypes); - string predName = result.Item1; - if (predName == _taskParam.FoodName) + await foreach ((ImageRegion pageRegion, Rect itemRect) in gridScreen) { - // 点击item - itemRegion.Click(); + using ImageRegion itemRegion = pageRegion.DeriveCrop(itemRect); + using Mat icon = itemRegion.SrcMat.GetGridIcon(); + var result = GridIconsAccuracyTestTask.Infer(icon, session, prototypes); + string predName = result.Item1; + if (predName == _taskParam.FoodName) + { + // 点击item + itemRegion.Click(); - #region 识别数量 - string numStr = itemRegion.SrcMat.GetGridItemIconText(OcrFactory.Paddle); - if (int.TryParse(numStr, out int num)) - { - count = num - 1; // 算上吃掉的1个 - } - else - { - count = -2; - _logger.LogWarning("无法识别食物数量:{text},依然尝试使用", numStr); - } - #endregion + #region 识别数量 + string numStr = itemRegion.SrcMat.GetGridItemIconText(OcrFactory.Paddle); + if (int.TryParse(numStr, out int num)) + { + count = num - 1; // 算上吃掉的1个 + } + else + { + count = -2; + _logger.LogWarning("无法识别食物数量:{text},依然尝试使用", numStr); + } + #endregion - await Delay(300, ct); - // 点击确定 - using var ra0 = CaptureToRectArea(); - using var ra = ra0.Find(ElementAssets.Instance.BtnWhiteConfirm); - if (ra.IsExist()) - { - ra.Click(); + await Delay(300, ct); + // 点击确定 + using var ra0 = CaptureToRectArea(); + using var ra = ra0.Find(ElementAssets.Instance.BtnWhiteConfirm); + if (ra.IsExist()) + { + ra.Click(); + } + _logger.LogInformation("吃了一份{name},真香!", predName); + break; } - _logger.LogInformation("吃了一份{name},真香!", predName); - break; } } + finally + { + VisionContext.Instance().DrawContent.ClearAll(); + } if (count == null) { count = -1; diff --git a/BetterGenshinImpact/GameTask/AutoFight/Assets/AutoFightAssets.cs b/BetterGenshinImpact/GameTask/AutoFight/Assets/AutoFightAssets.cs index 6fd55764..1264899e 100644 --- a/BetterGenshinImpact/GameTask/AutoFight/Assets/AutoFightAssets.cs +++ b/BetterGenshinImpact/GameTask/AutoFight/Assets/AutoFightAssets.cs @@ -2,6 +2,8 @@ using BetterGenshinImpact.Core.Recognition; using BetterGenshinImpact.GameTask.Model; using OpenCvSharp; using System.Collections.Generic; +using BetterGenshinImpact.GameTask.AutoGeniusInvokation.Model; + namespace BetterGenshinImpact.GameTask.AutoFight.Assets; @@ -11,6 +13,8 @@ public class AutoFightAssets : BaseAssets public Rect TeamRect; public List AvatarSideIconRectList; // 侧边栏角色头像 非联机状态下 public List AvatarIndexRectList; // 侧边栏角色头像对应的白色块 非联机状态下 + public List AvatarQRectListMap; // 角色头像对应的Q技能图标 + public Rect ERect; public Rect ECooldownRect; public Rect QRect; @@ -41,7 +45,19 @@ public class AutoFightAssets : BaseAssets public RecognitionObject AbnormalIconRa; - private AutoFightAssets() +#pragma warning disable CS8618 // 在退出构造函数时,不可为 null 的字段必须包含非 null 值。请考虑添加 "required" 修饰符或声明为可为 null。 + private AutoFightAssets() : base() + { + Initialization(this.systemInfo); + } + + protected AutoFightAssets(ISystemInfo systemInfo) : base(systemInfo) + { + Initialization(systemInfo); + } +#pragma warning restore CS8618 // 在退出构造函数时,不可为 null 的字段必须包含非 null 值。请考虑添加 "required" 修饰符或声明为可为 null。 + + private void Initialization(ISystemInfo systemInfo) { TeamRectNoIndex = new Rect(CaptureRect.Width - (int)(355 * AssetScale), (int)(220 * AssetScale), (int)((355 - 85) * AssetScale), (int)(465 * AssetScale)); @@ -72,6 +88,14 @@ public class AutoFightAssets : BaseAssets new Rect(CaptureRect.Width - (int)(61 * AssetScale), (int)(544 * AssetScale), (int)(28 * AssetScale), (int)(24 * AssetScale)), ]; + AvatarQRectListMap = + [ + new Rect(CaptureRect.Width - (int)(336 * AssetScale), (int)(216 * AssetScale), (int)(64 * AssetScale), (int)(84 * AssetScale)), + new Rect(CaptureRect.Width - (int)(336 * AssetScale), (int)(316 * AssetScale), (int)(64 * AssetScale), (int)(84 * AssetScale)), + new Rect(CaptureRect.Width - (int)(336 * AssetScale), (int)(416 * AssetScale), (int)(64 * AssetScale), (int)(84 * AssetScale)), + new Rect(CaptureRect.Width - (int)(336 * AssetScale), (int)(516 * AssetScale), (int)(64 * AssetScale), (int)(84 * AssetScale)), + ]; + AvatarSideIconRectList = [ new Rect(CaptureRect.Width - (int)(155 * AssetScale), (int)(225 * AssetScale), (int)(76 * AssetScale), (int)(76 * AssetScale)), @@ -156,7 +180,7 @@ public class AutoFightAssets : BaseAssets { Name = "1P", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "1p.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "1p.png", this.systemInfo), RegionOfInterest = new Rect(0, 0, CaptureRect.Width / 4, CaptureRect.Height / 7), DrawOnWindow = false }.InitTemplate(); @@ -165,7 +189,7 @@ public class AutoFightAssets : BaseAssets { Name = "P", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "p.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "p.png", this.systemInfo), RegionOfInterest = new Rect(CaptureRect.Width - (int)(CaptureRect.Width / 12.5), CaptureRect.Height / 5, (int)(CaptureRect.Width / 12.5), CaptureRect.Height / 2 - CaptureRect.Width / 7), DrawOnWindow = false }.InitTemplate(); @@ -174,14 +198,14 @@ public class AutoFightAssets : BaseAssets { Name = "WandererIcon", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "wanderer_icon.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "wanderer_icon.png", this.systemInfo), DrawOnWindow = false }.InitTemplate(); WandererIconNoActiveRa = new RecognitionObject { Name = "WandererIconNoActive", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "wanderer_icon_no_active.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "wanderer_icon_no_active.png", this.systemInfo), DrawOnWindow = false }.InitTemplate(); @@ -190,7 +214,7 @@ public class AutoFightAssets : BaseAssets { Name = "Confirm", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "confirm.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "confirm.png", this.systemInfo), RegionOfInterest = new Rect(CaptureRect.Width / 2, CaptureRect.Height / 2, CaptureRect.Width / 2, CaptureRect.Height / 2), DrawOnWindow = false }.InitTemplate(); @@ -198,7 +222,7 @@ public class AutoFightAssets : BaseAssets { Name = "ArtifactArea", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "artifact_flower_logo.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "artifact_flower_logo.png", this.systemInfo), RegionOfInterest = new Rect(CaptureRect.Width / 2, 0, CaptureRect.Width / 2, CaptureRect.Height), DrawOnWindow = false }.InitTemplate(); @@ -208,7 +232,7 @@ public class AutoFightAssets : BaseAssets { Name = "ClickAnyCloseTip", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "click_any_close_tip.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "click_any_close_tip.png", this.systemInfo), RegionOfInterest = new Rect(0, CaptureRect.Height / 2, CaptureRect.Width, CaptureRect.Height / 2), DrawOnWindow = false }.InitTemplate(); @@ -217,7 +241,7 @@ public class AutoFightAssets : BaseAssets { Name = "Exit", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "exit.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "exit.png", this.systemInfo), RegionOfInterest = new Rect(0, CaptureRect.Height / 2, CaptureRect.Width / 2, CaptureRect.Height / 2), DrawOnWindow = false }.InitTemplate(); @@ -227,7 +251,7 @@ public class AutoFightAssets : BaseAssets // { // Name = "LockIcon", // RecognitionType = RecognitionTypes.TemplateMatch, - // TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "lock_icon.png"), + // TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "lock_icon.png", this.systemInfo), // RegionOfInterest = new Rect(CaptureRect.Width - (int)(215 * AssetScale), 0, (int)(215 * AssetScale), (int)(80 * AssetScale)), // DrawOnWindow = false // }.InitTemplate(); @@ -236,7 +260,7 @@ public class AutoFightAssets : BaseAssets { Name = "AbnormalIcon", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "abnormal_icon.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "abnormal_icon.png", this.systemInfo), RegionOfInterest = new Rect(0, (int)(CaptureRect.Height * 0.08), (int)(CaptureRect.Width * 0.04), (int)(CaptureRect.Height * 0.07)), DrawOnWindow = false }.InitTemplate(); diff --git a/BetterGenshinImpact/GameTask/AutoFight/Assets/combat_avatar.json b/BetterGenshinImpact/GameTask/AutoFight/Assets/combat_avatar.json index 28278b5f..a59391f7 100644 --- a/BetterGenshinImpact/GameTask/AutoFight/Assets/combat_avatar.json +++ b/BetterGenshinImpact/GameTask/AutoFight/Assets/combat_avatar.json @@ -47,6 +47,26 @@ "nameEn": "PlayerBoy", "weapon": "1" }, + { + "alias": [ + "奇偶(男)", + "MannequinBoy" + ], + "id": "20000001", + "name": "奇偶(男)", + "nameEn": "MannequinBoy", + "weapon": "1" + }, + { + "alias": [ + "奇偶(女)", + "MannequinGirl" + ], + "id": "20000002", + "name": "奇偶(女)", + "nameEn": "MannequinGirl", + "weapon": "1" + }, { "alias": [ "神里绫华", @@ -1949,5 +1969,17 @@ "nameEn": "Aino", "skillCD": 10, "weapon": "11" + }, + { + "alias": [ + "奈芙尔", + "Nefer" + ], + "burstCD": 15, + "id": "10000122", + "name": "奈芙尔", + "nameEn": "Nefer", + "skillCD": 9, + "weapon": "10" } ] \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/AutoFight/AutoFightConfig.cs b/BetterGenshinImpact/GameTask/AutoFight/AutoFightConfig.cs index 557c7fa0..0b269de8 100644 --- a/BetterGenshinImpact/GameTask/AutoFight/AutoFightConfig.cs +++ b/BetterGenshinImpact/GameTask/AutoFight/AutoFightConfig.cs @@ -83,7 +83,24 @@ public partial class AutoFightConfig : ObservableObject /// [ObservableProperty] private string _beforeDetectDelay = ""; - + + /// + /// 旋转寻找敌人位置的旋转因子,默认为5,越大越快。 + /// + [ObservableProperty] + private int _rotaryFactor = 10; + + /// + /// 是否是第一次检查和面敌。 + /// + [ObservableProperty] + private bool _isFirstCheck = false; + + /// + /// 是有元素爆发前检查战斗结束 + /// + [ObservableProperty] + private bool _checkBeforeBurst = false; } /// /// 战斗结束相关配置 @@ -114,7 +131,10 @@ public partial class AutoFightConfig : ObservableObject private bool _kazuhaPickupEnabled = true; [ObservableProperty] - private string _guardianAvatar = " "; + private bool _qinDoublePickUp = false; + + [ObservableProperty] + private string _guardianAvatar = string.Empty; [ObservableProperty] private bool _guardianCombatSkip = false; @@ -125,12 +145,17 @@ public partial class AutoFightConfig : ObservableObject [ObservableProperty] private bool _guardianAvatarHold = false; + [ObservableProperty] + private bool _burstEnabled = false; + /// /// 战斗结束后,如果不存在万叶,则切换至存在万叶的队伍(基于开启万叶拾取情况下) /// [ObservableProperty] private string _kazuhaPartyName = ""; + [ObservableProperty] + private bool _swimmingEnabled = false; /// /// 战斗超时,单位秒 diff --git a/BetterGenshinImpact/GameTask/AutoFight/AutoFightParam.cs b/BetterGenshinImpact/GameTask/AutoFight/AutoFightParam.cs index 3baf94c5..d0509b53 100644 --- a/BetterGenshinImpact/GameTask/AutoFight/AutoFightParam.cs +++ b/BetterGenshinImpact/GameTask/AutoFight/AutoFightParam.cs @@ -1,3 +1,4 @@ +using BetterGenshinImpact.Core.Config; using BetterGenshinImpact.GameTask.Model; namespace BetterGenshinImpact.GameTask.AutoFight; @@ -47,8 +48,14 @@ public class AutoFightParam : BaseTaskParam GuardianAvatar = autoFightConfig.GuardianAvatar; GuardianCombatSkip = autoFightConfig.GuardianCombatSkip; - SkipModel = autoFightConfig.SkipModel; GuardianAvatarHold = autoFightConfig.GuardianAvatarHold; + BurstEnabled = autoFightConfig.BurstEnabled; + + CheckBeforeBurst = autoFightConfig.FinishDetectConfig.CheckBeforeBurst; + IsFirstCheck = autoFightConfig.FinishDetectConfig.IsFirstCheck; + RotaryFactor = autoFightConfig.FinishDetectConfig.RotaryFactor; + QinDoublePickUp = autoFightConfig.QinDoublePickUp; + SwimmingEnabled = autoFightConfig.SwimmingEnabled; } public FightFinishDetectConfig FinishDetectConfig { get; set; } = new(); @@ -65,8 +72,73 @@ public class AutoFightParam : BaseTaskParam public string ActionSchedulerByCd = ""; public string KazuhaPartyName; public string OnlyPickEliteDropsMode = ""; - public string GuardianAvatar { get; set; } = " "; + public string GuardianAvatar { get; set; } = string.Empty; public bool GuardianCombatSkip { get; set; } = false; - public bool SkipModel = false; public bool GuardianAvatarHold = false; + + public bool CheckBeforeBurst { get; set; } = false; + public bool IsFirstCheck { get; set; } = true; + public int RotaryFactor { get; set; } = 10; + public bool BurstEnabled { get; set; } = false; + + public bool QinDoublePickUp { get; set; } = false; + public static bool SwimmingEnabled { get; set; } = false; + + public AutoFightParam(string? strategyName = null) : base(null, null) + { + SetCombatStrategyPath(strategyName); + SetDefault(); + } + + /// + /// 设置战斗策略路径 + /// + /// 策略名称 + public void SetCombatStrategyPath(string? strategyName = null) + { + if (string.IsNullOrEmpty(strategyName)) + { + strategyName = TaskContext.Instance().Config.AutoFightConfig.StrategyName; + } + + if ("根据队伍自动选择".Equals(strategyName)) + { + CombatStrategyPath = Global.Absolute(@"User\AutoFight\"); + } + else + { + CombatStrategyPath = Global.Absolute(@"User\AutoFight\" + strategyName + ".txt"); + } + } + + public void SetDefault() + { + var autoFightConfig = TaskContext.Instance().Config.AutoFightConfig; + Timeout = autoFightConfig.Timeout; + FightFinishDetectEnabled = autoFightConfig.FightFinishDetectEnabled; + PickDropsAfterFightEnabled = autoFightConfig.PickDropsAfterFightEnabled; + PickDropsAfterFightSeconds = autoFightConfig.PickDropsAfterFightSeconds; + KazuhaPickupEnabled = autoFightConfig.KazuhaPickupEnabled; + ActionSchedulerByCd = autoFightConfig.ActionSchedulerByCd; + + FinishDetectConfig.FastCheckEnabled = autoFightConfig.FinishDetectConfig.FastCheckEnabled; + FinishDetectConfig.FastCheckParams = autoFightConfig.FinishDetectConfig.FastCheckParams; + FinishDetectConfig.CheckEndDelay = autoFightConfig.FinishDetectConfig.CheckEndDelay; + FinishDetectConfig.BeforeDetectDelay = autoFightConfig.FinishDetectConfig.BeforeDetectDelay; + FinishDetectConfig.RotateFindEnemyEnabled = autoFightConfig.FinishDetectConfig.RotateFindEnemyEnabled; + + + KazuhaPartyName = autoFightConfig.KazuhaPartyName; + OnlyPickEliteDropsMode = autoFightConfig.OnlyPickEliteDropsMode; + BattleThresholdForLoot = autoFightConfig.BattleThresholdForLoot ?? BattleThresholdForLoot; + //下面参数固定,只取自动战斗里面的 + FinishDetectConfig.BattleEndProgressBarColor = autoFightConfig.FinishDetectConfig.BattleEndProgressBarColor; + FinishDetectConfig.BattleEndProgressBarColorTolerance = autoFightConfig.FinishDetectConfig.BattleEndProgressBarColorTolerance; + + GuardianAvatar = autoFightConfig.GuardianAvatar; + GuardianCombatSkip = autoFightConfig.GuardianCombatSkip; + GuardianAvatarHold = autoFightConfig.GuardianAvatarHold; + SwimmingEnabled = autoFightConfig.SwimmingEnabled; + QinDoublePickUp = autoFightConfig.QinDoublePickUp; + } } \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/AutoFight/AutoFightSeek.cs b/BetterGenshinImpact/GameTask/AutoFight/AutoFightSeek.cs index 3482b4d5..06b4f011 100644 --- a/BetterGenshinImpact/GameTask/AutoFight/AutoFightSeek.cs +++ b/BetterGenshinImpact/GameTask/AutoFight/AutoFightSeek.cs @@ -8,6 +8,14 @@ using OpenCvSharp; using BetterGenshinImpact.Core.Recognition.OpenCv; using BetterGenshinImpact.GameTask.AutoFight.Model; using BetterGenshinImpact.GameTask.AutoFight.Script; +using System; +using BetterGenshinImpact.GameTask.AutoFight.Assets; +using System.Linq; +using System.Collections.Generic; +using BetterGenshinImpact.GameTask.Common.BgiVision; +using BetterGenshinImpact.GameTask.Common.Element.Assets; +using OpenCvSharp; +using BetterGenshinImpact.GameTask.Model.Area; namespace BetterGenshinImpact.GameTask.AutoFight { @@ -21,20 +29,20 @@ namespace BetterGenshinImpact.GameTask.AutoFight public static Task MoveForwardAsync(Scalar scalarLower, Scalar scalarHigher, ILogger logger, CancellationToken ct) { - var image2 = CaptureToRectArea(); - Mat mask2 = OpenCvCommonHelper.Threshold( + using var image2 = CaptureToRectArea(); + using Mat mask2 = OpenCvCommonHelper.Threshold( image2.DeriveCrop(0, 0, image2.Width * 1570 / 1920, image2.Height * 970 / 1080).SrcMat, scalarLower, scalarHigher ); - Mat labels2 = new Mat(); - Mat stats2 = new Mat(); - Mat centroids2 = new Mat(); + using Mat labels2 = new Mat(); + using Mat stats2 = new Mat(); + using Mat centroids2 = new Mat(); int numLabels2 = Cv2.ConnectedComponentsWithStats(mask2, labels2, stats2, centroids2, connectivity: PixelConnectivity.Connectivity4, ltype: MatType.CV_32S); - logger.LogInformation("检测数量:{numLabels2}", numLabels2 - 1); + // logger.LogInformation("检测数量:{numLabels2}", numLabels2 - 1); if (numLabels2 > 1) { @@ -153,10 +161,12 @@ namespace BetterGenshinImpact.GameTask.AutoFight // 非上述区域且非中心区域,判断左右 if (firstPixel.X < 920 && height > 6) { + Simulation.SendInput.SimulateAction(GIActions.MoveBackward); logger.LogInformation("敌人在左侧,不移动"); } else if (firstPixel.X > 920 && height > 6) { + Simulation.SendInput.SimulateAction(GIActions.MoveBackward); logger.LogInformation("敌人在右侧,不移动"); } } @@ -165,6 +175,7 @@ namespace BetterGenshinImpact.GameTask.AutoFight { if (height > 6) { + Simulation.SendInput.SimulateAction(GIActions.MoveBackward); logger.LogInformation("敌人在中心且高度大于6,不移动"); } else if (firstPixel.X < 1315 && firstPixel.X > 500 && firstPixel.Y < 800 && height > 2) @@ -189,10 +200,12 @@ namespace BetterGenshinImpact.GameTask.AutoFight } else if (height < 3) { + Simulation.SendInput.SimulateAction(GIActions.MoveBackward); logger.LogInformation("敌人血量高度小于3,不移动"); } else { + Simulation.SendInput.SimulateAction(GIActions.MoveBackward); logger.LogInformation("不移动"); } } @@ -202,9 +215,7 @@ namespace BetterGenshinImpact.GameTask.AutoFight logger.LogError("无法获取统计信息数组"); } } - - mask2.Dispose(); - labels2.Dispose(); + return Task.FromResult(null); } } @@ -213,54 +224,97 @@ namespace BetterGenshinImpact.GameTask.AutoFight { public static int RotationCount = 0; - public static async Task SeekAndFightAsync(ILogger logger, int detectDelayTime,int delayTime,CancellationToken ct) + private static readonly Dictionary RotaryFactorMapping = new Dictionary //旋转因子映射表 + { + { 1, 100 }, { 2, 90 }, { 3, 80}, { 4, 70 }, { 5, 60}, { 6,45 }, + { 7, 30 }, { 8, 15 }, { 9, 6 }, { 10, 1 }, { 11,-10 }, { 12,-50 }, { 13, -60 } + }; + + public static async Task SeekAndFightAsync(ILogger logger, int detectDelayTime,int delayTime,CancellationToken ct,bool isEndCheck = false,int rotaryFactor = 6) { Scalar bloodLower = new Scalar(255, 90, 90); - int retryCount = 0; + + var adjustedX = RotaryFactorMapping[rotaryFactor]; + var adjustedDivisor = rotaryFactor<=12 ? 2 : 1.3; + + // Logger.LogInformation("开始寻找敌人 {Text} ...",adjustedX); + + int retryCount = isEndCheck? 1 : 0; - while (retryCount < 27) + while (retryCount < 25+(int)(adjustedX / 5)) { var image = CaptureToRectArea(); Mat mask = OpenCvCommonHelper.Threshold(image.DeriveCrop(0, 0, 1500, 900).SrcMat, bloodLower); + Mat labels = new Mat(); Mat stats = new Mat(); Mat centroids = new Mat(); int numLabels = Cv2.ConnectedComponentsWithStats(mask, labels, stats, centroids, connectivity: PixelConnectivity.Connectivity4, ltype: MatType.CV_32S); - if (retryCount == 0) logger.LogInformation("敌人初检数量: {numLabels}", numLabels - 1); + // if (retryCount == 0) logger.LogInformation("敌人初检数量: {numLabels}", numLabels - 1); if (numLabels > 1) { - logger.LogInformation("检测画面内疑似有敌人,继续战斗..."); + // logger.LogInformation("检测画面内疑似有敌人,继续战斗..."); - Mat firstRow = stats.Row(1); + using Mat firstRow = stats.Row(1); int[] statsArray; bool success = firstRow.GetArray(out statsArray); int height = statsArray[3]; - logger.LogInformation("敌人血量高度:{height}", height); + int x = statsArray[0]; + // Logger.LogInformation("敌人位置: ({x},血量高度: {height}", x, height); + image.Dispose(); mask.Dispose(); labels.Dispose(); stats.Dispose(); centroids.Dispose(); - if (success && height > 2) + if (success) { - if (height < 7) + if (isEndCheck) { - logger.LogInformation("敌人血量高度小于7且大于2,向前移动"); - Task.Run(() => + await Task.Run(() => { - MoveForwardTask.MoveForwardAsync(bloodLower, bloodLower, logger, ct); Simulation.SendInput.SimulateAction(GIActions.MoveForward, KeyType.KeyDown); - Task.Delay(1000, ct).Wait(); + Task.Delay(100, ct).Wait();; Simulation.SendInput.SimulateAction(GIActions.MoveForward, KeyType.KeyUp); }, ct); } - return false; + else + { + Simulation.SendInput.SimulateAction(GIActions.MoveForward); + Simulation.SendInput.SimulateAction(GIActions.MoveForward); + } + + if (height > 2 && height < 7) + { + // logger.LogInformation("画面内有找到敌人,尝试移动..."); + Task.Run(() => { MoveForwardTask.MoveForwardAsync(bloodLower, bloodLower, logger, ct); }, ct); + return false; + } + + if (height > 6 && height < 25) + { + if ((x == 758 || x == 722) && (height ==7 || height == 8))//固定血条的怪物,尝试旋转寻找 + { + await Task.Run(() => + { + Simulation.SendInput.Mouse.MoveMouseBy(960, 0); + Task.Delay(200, ct).Wait(); + Simulation.SendInput.Mouse.MiddleButtonClick(); + }, ct); + } + // logger.LogInformation("画面内有找到敌人,继续战斗..."); + return false; + } + + if (height < 3 || height > 25) + { + return null; + } } - if (height < 3) return null; } if (retryCount == 0) @@ -273,11 +327,12 @@ namespace BetterGenshinImpact.GameTask.AutoFight var b33 = ra3.SrcMat.At(50, 790); // 进度条颜色 var whiteTile3 = ra3.SrcMat.At(50, 768); // 白块 Simulation.SendInput.SimulateAction(GIActions.Drop); + ra3.Dispose(); if (IsWhite(whiteTile3.Item2, whiteTile3.Item1, whiteTile3.Item0) && IsYellow(b33.Item2, b33.Item1, b33.Item0)) { - logger.LogInformation("识别到战斗结束"); + logger.LogInformation("识别到战斗结束-s"); Simulation.SendInput.SimulateAction(GIActions.OpenPartySetupScreen); return true; } @@ -292,12 +347,13 @@ namespace BetterGenshinImpact.GameTask.AutoFight if (retryCount <= 2) { var offsets = new (int x, int y)[] { - (image.Width / 6, -image.Height / 5), + (image.Width / 6, image.Height / 7), (image.Width / 6, 0), - (image.Width / 6, image.Height / 6) + (image.Width / 6, -image.Height / 5), + (image.Width / 6, -image.Height), }; - var offsetIndex = RotationCount < 3 ? 0 : (RotationCount == 3) ? 1 : 2; + var offsetIndex = RotationCount < 2 ? 0 : (RotationCount == 2) ? 1 : (RotationCount >= 3) ? 2 : 3; Simulation.SendInput.Mouse.MoveMouseBy(offsets[offsetIndex].x, offsets[offsetIndex].y); } else @@ -305,7 +361,7 @@ namespace BetterGenshinImpact.GameTask.AutoFight Simulation.SendInput.Mouse.MoveMouseBy(image.Width / 6, 0); } - await Task.Delay(50,ct); + await Task.Delay(50+(int)(adjustedX/adjustedDivisor),ct); image = CaptureToRectArea(); mask = OpenCvCommonHelper.Threshold(image.DeriveCrop(0, 0, 1500, 900).SrcMat, bloodLower); @@ -318,34 +374,57 @@ namespace BetterGenshinImpact.GameTask.AutoFight if (numLabels > 1) { - logger.LogInformation("检测敌人第 {retryCount} 次: {numLabels}", retryCount + 1, numLabels - 1); + // logger.LogInformation("检测敌人第 {retryCount} 次: {numLabels}", retryCount + 1, numLabels - 1); Mat firstRow2 = stats.Row(1); // 获取第1行(标签1)的数据 int[] statsArray2; bool success2 = firstRow2.GetArray(out statsArray2); // 使用 out 参数来接收数组数据 int height2 = statsArray2[3]; - logger.LogInformation("敌人血量高度:{height2}", height2); + // logger.LogInformation("敌人血量 :{height2}", height2); mask.Dispose(); labels.Dispose(); stats.Dispose(); centroids.Dispose(); - - if (success2 && height2 > 2) + image.Dispose(); + + if (success2) { - if (height2 < 7) + if (isEndCheck) await Task.Run(() => { - logger.LogInformation("画面内有找到敌人,继续战斗..."); - Task.Run(() => { MoveForwardTask.MoveForwardAsync(bloodLower, bloodLower, logger, ct); }, ct); + Simulation.SendInput.SimulateAction(GIActions.MoveForward, KeyType.KeyDown); + Task.Delay(100, ct).Wait(); + Simulation.SendInput.SimulateAction(GIActions.MoveForward, KeyType.KeyUp); + }, ct); + + if (height2 > 2 && height2 < 7) + { + // logger.LogInformation("画面内有找到敌人,尝试移动..."); + Task.Run(() => { MoveForwardTask.MoveForwardAsync(bloodLower, bloodLower, logger, ct); }, ct); + return false; + } + + if (height2 > 6 && height2 < 25) + { + // logger.LogInformation("画面内有找到敌人,继续战斗..."); + return false; + } + + if (height2 < 3 || height2 > 25) + { + return null; } - return false; } - - if (height2 < 3) return null; - } + mask.Dispose(); + labels.Dispose(); + stats.Dispose(); + centroids.Dispose(); + image.Dispose(); + retryCount++; } + logger.LogInformation("寻找敌人:{Text}", "无"); return null; } @@ -371,103 +450,271 @@ namespace BetterGenshinImpact.GameTask.AutoFight public class AutoFightSkill { - public static async Task EnsureGuardianSkill(Avatar guardianAvatar, CombatCommand command,string lastFightName, - string guardianAvatarName,bool guardianAvatarHold,int retryCount,CancellationToken ct) + public static async Task EnsureGuardianSkill(Avatar guardianAvatar, CombatCommand command, string lastFightName, + string guardianAvatarName, bool guardianAvatarHold, int retryCount, CancellationToken ct,bool guardianCombatSkip = false, + bool burstEnabled = false) { int attempt = 0; - + if (guardianAvatar.IsSkillReady()) { while (attempt < retryCount) { - if (guardianAvatar.TrySwitch(15,false)) + if (guardianAvatar.TrySwitch(10, false)) { - Simulation.ReleaseAllKey(); - - await Task.Delay(100, ct); - guardianAvatar.ManualSkillCd = -1; - var cd1 = guardianAvatar.AfterUseSkill(); - if (cd1 > 0 ) + if (await AvatarSkillAsync(Logger, guardianAvatar, false, 1, ct)) { - Logger.LogInformation("优先第 {text} 盾奶位 {GuardianAvatar} 战技Cd检测:{cd} 秒", guardianAvatarName, guardianAvatar.Name, cd1); - guardianAvatar.ManualSkillCd = -1; - return; + var cd1 = guardianAvatar.AfterUseSkill(); + if (cd1 > 0) + { + Logger.LogInformation("优先第 {text} 盾奶位 {GuardianAvatar} 战技Cd检测:{cd} 秒", guardianAvatarName, + guardianAvatar.Name, cd1); + guardianAvatar.ManualSkillCd = -1; + return; + } } - + guardianAvatar.UseSkill(guardianAvatarHold); - Simulation.ReleaseAllKey(); - await Task.Delay(200, ct); + var imageAfterUseSkill = CaptureToRectArea(); - var cd2 = guardianAvatar.AfterUseSkill(); - if ( cd2 > 0 && guardianAvatar.TrySwitch(4,false)) + var retry = 50; + while (!(await AvatarSkillAsync(Logger, guardianAvatar, false, 1, ct,imageAfterUseSkill)) && retry > 0) { - Logger.LogInformation("优先第 {text} 盾奶位 {GuardianAvatar} 释放战技成功,cd:{cd2} 秒", guardianAvatarName, guardianAvatar.Name, cd2); + Simulation.SendInput.SimulateAction(GIActions.ElementalSkill); + //防止在纳塔飞天或爬墙 + Simulation.ReleaseAllKey(); + if (retry % 3 == 0) + { + Simulation.SendInput.SimulateAction(GIActions.NormalAttack); + Simulation.SendInput.SimulateAction(GIActions.Drop); + } + imageAfterUseSkill = CaptureToRectArea(); + await Task.Delay(30, ct); + // Logger.LogInformation("优先第333 {t}", retry); + retry -= 1; + } + imageAfterUseSkill.Dispose(); + + if (retry > 0) + { + Logger.LogInformation("优先第 {text} 盾奶位 {GuardianAvatar} 释放战技:{t}", + guardianAvatarName, guardianAvatar.Name,"成功"); + guardianAvatar.LastSkillTime = DateTime.UtcNow; guardianAvatar.ManualSkillCd = -1; return; } - //新方法法:色块识别,带角色切换确认,不管OCR结果。避免OCR技能CD错误 - // if (await AvatarSkillAsync(Logger, guardianAvatar,false, 5, ct)) - // { - // guardianAvatar.ManualSkillCd = -1; - // return; - // } - - Logger.LogInformation("优先第 {text} 盾奶位 {GuardianAvatar} 释放战技:失败重试 {attempt} 次", guardianAvatarName, guardianAvatar.Name , attempt+1); + Logger.LogInformation("优先第 {text} 盾奶位 {GuardianAvatar} 释放战技:失败重试 {attempt} 次", + guardianAvatarName, guardianAvatar.Name, attempt + 1); guardianAvatar.ManualSkillCd = 0; + guardianAvatar.UseSkill(guardianAvatarHold); + //防止在纳塔飞天或 + Simulation.SendInput.SimulateAction(GIActions.NormalAttack); + Simulation.SendInput.SimulateAction(GIActions.Drop); } attempt++; } - } - } + } + else if (burstEnabled) + { + using var image = CaptureToRectArea(); + if (!guardianAvatar.IsActive(image)) + { + var skillArea = AutoFightAssets.Instance.AvatarQRectListMap[guardianAvatar.Index - 1];//Q技能区域 + // 首先对图像进行预处理,转为灰度图 + using var grayImage = image.DeriveCrop(skillArea).SrcMat.CvtColor(ColorConversionCodes.BGR2GRAY); + + //调试用 + // grayImage.SaveImage("D:\\Images\\grayImage.png"); + // Cv2.ImShow("灰度图像", grayImage); + + // 计算图像的平均亮度 + var meanBrightness = Cv2.Mean(grayImage); + var avgBrightness = meanBrightness.Val0; + // 根据平均亮度动态调整Canny边缘检测的阈值 + var threshold1 = avgBrightness * 0.9; + var threshold2 = avgBrightness * 2; + + // Logger.LogInformation("角色{i} 平均亮度 {avgBrightness}", i, avgBrightness); + + Cv2.Canny(grayImage, grayImage, threshold1: (float)threshold1, threshold2: (float)threshold2); // 边缘检测 + + // 使用霍夫变换检测圆形 + var circles = Cv2.HoughCircles(grayImage, HoughModes.Gradient, dp: 1.2, minDist: 20, + param1: 70, param2: 30, minRadius: 25, maxRadius: 34); + if (circles.Length > 0) + { + Logger.LogInformation("优先第 {text} 盾奶位 {GuardianAvatar} 元素爆发状态:{attempt},尝试释放", + guardianAvatarName, guardianAvatar.Name, "就绪"); + + if (guardianAvatar.TrySwitch(8, false)) + { + Simulation.SendInput.SimulateAction(GIActions.ElementalBurst); + Sleep(500, ct); + Simulation.ReleaseAllKey(); + + //普攻一下,防止在纳塔飞天 + Simulation.SendInput.SimulateAction(GIActions.NormalAttack); + using (var imageAfterBurst = CaptureToRectArea()) + { + if (AvatarSkillAsync(Logger, guardianAvatar, true, 1, ct).Result + || !Bv.IsInMainUi(imageAfterBurst)) //Q技能CD(冷却检测)或者不在主界面(大招动画播放中) + { + guardianAvatar.IsBurstReady = false; + } + else + { + Sleep(500, ct); + Simulation.SendInput.SimulateAction(GIActions.NormalAttack);//普攻一下,防止在纳塔飞天 + Simulation.SendInput.SimulateAction(GIActions.ElementalBurst);//尝试再放一次,不检查 + guardianAvatar.IsBurstReady = true; + } + Logger.LogInformation("优先第 {guardianAvatarName} 盾奶位 {GuardianAvatar} 释放元素爆发:{text}", + guardianAvatarName, guardianAvatar.Name, !guardianAvatar.IsBurstReady ? "成功" : "失败"); + } + } + } + } + } + } + //新方法,备用,非OCR识别,判断色块进行,速度更快 - //检测技能图标中释放含有白色色块,检测前进行角色切换的确认,skills:false为E技能,true为Q技能(未开发) - public static async Task AvatarSkillAsync(ILogger logger, Avatar guardianAvatar, bool skills , int retryCount, CancellationToken ct) + //检测技能图标中释放含有白色色块,检测前进行角色切换的确认,skills:false为E技能,true为Q技能 + /// + /// 检测角色技能冷却状态 + /// + /// 日志记录器 + /// 角色对象 + /// 技能类型,false为E技能,true为Q技能 + /// 重试次数 + /// 取消令牌 + /// 图像对象 + /// 是否需要日志输出 + /// 是否重置技能冷却状态 + /// 技能是否就绪 + public static async Task AvatarSkillAsync(ILogger logger, Avatar guardianAvatar, bool skills , int retryCount, + CancellationToken ct,ImageRegion? image = null,bool needLog = false, bool isResetCd = false) { if (guardianAvatar.TrySwitch()) { Scalar bloodLower = new Scalar(255, 255, 255); int attempt = 0; + var model = image is null; while (attempt < retryCount) { - var image2 = CaptureToRectArea(); + using var image2 = model ? CaptureToRectArea() : image ?? CaptureToRectArea(); - var skillAra = skills - ? new Rect(image2.Width * 1700 / 1920, image2.Height * 996 / 1080, - image2.Width * 12 / 1920, image2.Height * 7 / 1080) //E技能区域 + // var image2 = CaptureToRectArea(); + + var skillAra = !skills + ? new Rect(image2.Width * 1688 / 1920, image2.Height * 988 / 1080, + image2.Width * 22 / 1920, image2.Height * 12 / 1080) //E技能区域 - : new Rect(image2.Width * 1819 / 1920, image2.Height * 977 / 1080, - image2.Width * 13 / 1920, image2.Height * 6 / 1080); //Q技能区域 + : new Rect(image2.Width * 1809 / 1920, image2.Height * 968 / 1080, + image2.Width * 30 / 1920, image2.Height * 15 / 1080); //Q技能区域 - var mask2 = OpenCvCommonHelper.Threshold( + using var mask2 = OpenCvCommonHelper.Threshold( image2.DeriveCrop(skillAra).SrcMat, bloodLower, bloodLower ); - var labels2 = new Mat(); - var stats2 = new Mat(); - var centroids2 = new Mat(); + using var labels2 = new Mat(); + using var stats2 = new Mat(); + using var centroids2 = new Mat(); int numLabels2 = Cv2.ConnectedComponentsWithStats(mask2, labels2, stats2, centroids2, connectivity: PixelConnectivity.Connectivity4, ltype: MatType.CV_32S); - logger.LogInformation("盾奶位 {guardianAvatar.Name} 战技状态:{text}", guardianAvatar.Name , numLabels2 > 1 ? "已释放" : "未释放"); - - if (numLabels2 > 1) + if (model) image2.Dispose(); + + if (needLog) Logger.LogInformation("技能状态:{guardianAvatar.Name} - {skills} 状态 {text}", + guardianAvatar.Name, skills?"Q技能":"E技能", numLabels2 > 1?"冷却中":"就绪"); + + // Logger.LogInformation("技能状态:{numLabels2} 数量", numLabels2); + if (numLabels2 > 2) { + if (!isResetCd) + { + return true; + } + if (skills) + { + guardianAvatar.IsBurstReady = true; + } + else + { + guardianAvatar.ManualSkillCd = 0; + } + return true; } - + attempt++; - await Task.Delay(100, ct); + if (retryCount > 1) await Task.Delay(100, ct); } } - guardianAvatar.AfterUseSkill(); + + if (!isResetCd) + { + return false; + } + + if (skills) + { + guardianAvatar.IsBurstReady = false; + } + else + { + guardianAvatar.AfterUseSkill(); + } + return false; + + } + + //全队Q检测函数,备用,后续可用于自动EQ开发 + public static Task> AvatarQSkillAsync(ImageRegion? image = null, List? useEqList = null,int? avatarCurrent = null) + { + image ??= CaptureToRectArea(); + image.SrcMat.ConvertTo(image.SrcMat, MatType.CV_8UC3, alpha: 2, beta: -200); // 增加亮度和对比度 + var useMedicine = new List(); + var eqList = useEqList ?? new List { 1, 2, 3, 4 }; + + foreach (var i in eqList) + { + var skillArea = i != avatarCurrent ? AutoFightAssets.Instance.AvatarQRectListMap[i - 1]: new Rect(1762, 915, 114, 111); + + using var grayImage = image.DeriveCrop(skillArea).SrcMat.CvtColor(ColorConversionCodes.BGR2GRAY); + + var meanBrightness = Cv2.Mean(grayImage); + var avgBrightness = meanBrightness.Val0; + var threshold1 = avgBrightness * 0.9; + var threshold2 = avgBrightness * 2; + + Cv2.Canny(grayImage, grayImage, threshold1: (float)threshold1, threshold2: (float)threshold2); + + var circles = Cv2.HoughCircles(grayImage, HoughModes.Gradient, dp: 1.2, minDist: 20, + param1: 90, param2:i != avatarCurrent ? 25 : 35, minRadius: i != avatarCurrent ? 25 : 50, maxRadius:i != avatarCurrent ? 34 : 60); + + if (circles.Length > 0) + { + useMedicine.Add(i); + } + } + + image.Dispose(); + + if (useMedicine.Count > 0) + { + Logger.LogInformation("元素爆发 {text} 的角色序号:{useMedicine}", "就绪", useMedicine); + return Task.FromResult(useMedicine); + } + + return Task.FromResult(new List()); } } diff --git a/BetterGenshinImpact/GameTask/AutoFight/AutoFightTask.cs b/BetterGenshinImpact/GameTask/AutoFight/AutoFightTask.cs index 228fd69a..d1328fcc 100644 --- a/BetterGenshinImpact/GameTask/AutoFight/AutoFightTask.cs +++ b/BetterGenshinImpact/GameTask/AutoFight/AutoFightTask.cs @@ -1,4 +1,4 @@ -using BetterGenshinImpact.Core.Recognition.ONNX; +using BetterGenshinImpact.Core.Recognition.ONNX; using BetterGenshinImpact.Core.Simulator; using BetterGenshinImpact.Core.Simulator.Extensions; using BetterGenshinImpact.GameTask.AutoFight.Model; @@ -13,13 +13,15 @@ using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; -using BetterGenshinImpact.Core.Config; using static BetterGenshinImpact.GameTask.Common.TaskControl; using BetterGenshinImpact.GameTask.Common.Job; using OpenCvSharp; using BetterGenshinImpact.Helpers; using Vanara; using Microsoft.Extensions.DependencyInjection; +using BetterGenshinImpact.GameTask.AutoPathing.Model; +using BetterGenshinImpact.GameTask.AutoPathing.Handler; +using BetterGenshinImpact.GameTask.AutoPick.Assets; namespace BetterGenshinImpact.GameTask.AutoFight; @@ -38,11 +40,14 @@ public class AutoFightTask : ISoloTask private DateTime _lastFightFlagTime = DateTime.Now; // 战斗标志最近一次出现的时间 private readonly double _dpi = TaskContext.Instance().DpiScale; - - public static OtherConfig Config { get; set; } = TaskContext.Instance().Config.OtherConfig; public static bool FightStatusFlag { get; set; } = false; + + private static readonly object PickLock = new object(); + // 战斗点位 + public static WaypointForTrack? FightWaypoint {get; set;} = null; + private class TaskFightFinishDetectConfig { public int DelayTime = 1500; @@ -279,6 +284,9 @@ public class AutoFightTask : ISoloTask //所有角色是否都可被跳过 var allCanBeSkipped = commandAvatarNames.All(a => canBeSkippedAvatarNames.Contains(a)); + var delayTime = _finishDetectConfig.DelayTime; + var detectDelayTime = _finishDetectConfig.DetectDelayTime; + //盾奶优先功能角色预处理 var guardianAvatar = string.IsNullOrWhiteSpace(_taskParam.GuardianAvatar) ? null : combatScenes.SelectAvatar(int.Parse(_taskParam.GuardianAvatar)); @@ -322,13 +330,23 @@ public class AutoFightTask : ISoloTask #region 盾奶位技能优先功能 - var skipModel = _taskParam.SkipModel? (guardianAvatar != null) : (guardianAvatar != null && lastFightName != command.Name); - if (skipModel) await AutoFightSkill.EnsureGuardianSkill(guardianAvatar,lastCommand,lastFightName,_taskParam.GuardianAvatar,_taskParam.GuardianAvatarHold,5,ct); + var skipModel = guardianAvatar != null && lastFightName != command.Name; + if (skipModel) await AutoFightSkill.EnsureGuardianSkill(guardianAvatar,lastCommand,lastFightName, + _taskParam.GuardianAvatar,_taskParam.GuardianAvatarHold,5,ct,_taskParam.GuardianCombatSkip,_taskParam.BurstEnabled); var avatar = combatScenes.SelectAvatar(command.Name); #endregion - if (avatar is null || (avatar.Name == guardianAvatar?.Name && _taskParam.GuardianCombatSkip)) + #region 初始寻敌处理 + + if ( _finishDetectConfig.RotateFindEnemyEnabled && i == 0 && _taskParam.IsFirstCheck) + { + await AutoFightSeek.SeekAndFightAsync(Logger, detectDelayTime, delayTime, ct,true,_taskParam.RotaryFactor); + } + + #endregion + + if (avatar is null || (avatar.Name == guardianAvatar?.Name && (_taskParam.GuardianCombatSkip || _taskParam.BurstEnabled))) { continue; } @@ -386,8 +404,15 @@ public class AutoFightTask : ISoloTask timeOutFlag = true; break; } + + #region Q前寻敌处理 + if (_finishDetectConfig.RotateFindEnemyEnabled && _taskParam.CheckBeforeBurst && (command.Method == Method.Burst || command.Args.Contains("q") || command.Args.Contains("Q"))) + { + fightEndFlag = await CheckFightFinish(delayTime, detectDelayTime); + } + #endregion - command.Execute(combatScenes); + command.Execute(combatScenes, lastCommand); //统计战斗人次 if (i == combatCommands.Count - 1 || command.Name != combatCommands[i + 1].Name) { @@ -408,8 +433,7 @@ public class AutoFightTask : ISoloTask )) { checkFightFinishStopwatch.Restart(); - var delayTime = _finishDetectConfig.DelayTime; - var detectDelayTime = _finishDetectConfig.DetectDelayTime; + if (_finishDetectConfig.DelayTimes.TryGetValue(command.Name, out var time)) { delayTime = time; @@ -460,11 +484,11 @@ public class AutoFightTask : ISoloTask if (_taskParam.KazuhaPickupEnabled) { // 队伍中存在万叶的时候使用一次长E - var kazuha = combatScenes.SelectAvatar("枫原万叶"); + var picker = combatScenes.SelectAvatar("枫原万叶") ?? combatScenes.SelectAvatar("琴"); var oldPartyName = RunnerContext.Instance.PartyName; var switchPartyFlag = false; - if (kazuha == null && !timeOutFlag &&!string.IsNullOrEmpty(_taskParam.KazuhaPartyName) && oldPartyName != _taskParam.KazuhaPartyName) + if (picker == null && !timeOutFlag &&!string.IsNullOrEmpty(_taskParam.KazuhaPartyName) && oldPartyName != _taskParam.KazuhaPartyName) { try { @@ -477,7 +501,7 @@ public class AutoFightTask : ISoloTask RunnerContext.Instance.PartyName = _taskParam.KazuhaPartyName; RunnerContext.Instance.ClearCombatScenes(); var cs = await RunnerContext.Instance.GetCombatScenes(ct); - kazuha = cs.SelectAvatar("枫原万叶"); + picker = cs.SelectAvatar("枫原万叶") ?? cs.SelectAvatar("琴"); } } catch (Exception e) @@ -486,28 +510,100 @@ public class AutoFightTask : ISoloTask } } - - if (kazuha != null) + if (picker != null) { - var time = TimeSpan.FromSeconds(kazuha.GetSkillCdSeconds()); - //当万叶cd大于3时,此时不再触发万叶拾取, - if (!(lastFightName == "枫原万叶" && time.TotalSeconds > 3)) + if (picker.Name == "枫原万叶") { - Logger.LogInformation("使用枫原万叶长E拾取掉落物"); - await Delay(300, ct); - if (kazuha.TrySwitch()) + var time = TimeSpan.FromSeconds(picker.GetSkillCdSeconds()); + if (!(lastFightName == picker.Name && time.TotalSeconds > 3)) { - await kazuha.WaitSkillCd(ct); - kazuha.UseSkill(true); - await Task.Delay(100); - Simulation.SendInput.SimulateAction(GIActions.NormalAttack); - await Delay(1500, ct); + Logger.LogInformation("使用 枫原万叶-长E 拾取掉落物"); + await Delay(200, ct); + if (picker.TrySwitch(10)) + { + await picker.WaitSkillCd(ct); + picker.UseSkill(true); + await Delay(50, ct); + Simulation.SendInput.SimulateAction(GIActions.NormalAttack); + await Delay(1500, ct); + } + } + else + { + Logger.LogInformation("距最近一次万叶出招,时间过短,跳过此次万叶拾取!"); } } - else + else if (picker.Name == "琴") { - Logger.LogInformation("距最近一次万叶出招,时间过短,跳过此次万叶拾取!"); + Logger.LogInformation("使用 琴-长E 拾取掉落物"); + + var actionsToUse = PickUpCollectHandler.PickUpActions + .Where(action => action.StartsWith("琴-长E" + " ", StringComparison.OrdinalIgnoreCase)) + .Select(action => action.Replace("琴-长E","琴", StringComparison.OrdinalIgnoreCase)) + .ToArray(); + + var find = _taskParam.QinDoublePickUp; + await Delay(150, ct); + if (picker.TrySwitch(10)) + { + foreach (var miningActionStr in actionsToUse) + { + var pickUpAction = CombatScriptParser.ParseContext(miningActionStr); + + for (int i = 0; i < 2; i++) + { + await picker.WaitSkillCd(ct); + foreach (var command in pickUpAction.CombatCommands) + { + command.Execute(combatScenes); + //异步执行,防止卡顿 + Task.Run(() => + { + if (Monitor.TryEnter(PickLock)) + { + try + { + if (find) + { + using (var imagePick = CaptureToRectArea()) + { + if (imagePick.Find(AutoPickAssets.Instance.PickRo).IsExist()) + { + find = false; + } + } + } + } + finally + { + Monitor.Exit(PickLock); + } + } + // 后面没代码了,不用写return? + }); + } + + if (!find) + { + break; + } + + if (i == 0) + { + Logger.LogInformation("自动拾取;尝试再次执行 琴-长E 拾取"); + // picker.LastSkillTime = DateTime.Now;不正确 + picker.AfterUseSkill(); + } + else + { + break; + } + } + + Simulation.ReleaseAllKey(); + } + } } } //切换过队伍的,需要再切回来 @@ -586,7 +682,7 @@ public class AutoFightTask : ISoloTask Simulation.SendInput.SimulateAction(GIActions.OpenPartySetupScreen); await Delay(detectDelayTime, _ct); - var ra = CaptureToRectArea(); + using var ra = CaptureToRectArea(); //判断整个界面是否有红色色块,如果有,则战继续,否则战斗结束 // 只提取橙色 diff --git a/BetterGenshinImpact/GameTask/AutoFight/Model/Avatar.cs b/BetterGenshinImpact/GameTask/AutoFight/Model/Avatar.cs index e4fc4281..3f3a62fe 100644 --- a/BetterGenshinImpact/GameTask/AutoFight/Model/Avatar.cs +++ b/BetterGenshinImpact/GameTask/AutoFight/Model/Avatar.cs @@ -20,6 +20,10 @@ using static BetterGenshinImpact.GameTask.Common.TaskControl; using BetterGenshinImpact.Core.Config; using BetterGenshinImpact.GameTask.AutoFight.Assets; using BetterGenshinImpact.ViewModel.Pages; +using BetterGenshinImpact.GameTask.AutoGeniusInvokation.Model; +using BetterGenshinImpact.GameTask.AutoPathing; +using BetterGenshinImpact.GameTask.AutoPathing.Model; +using BetterGenshinImpact.GameTask.AutoPathing.Model.Enum; namespace BetterGenshinImpact.GameTask.AutoFight.Model; @@ -82,9 +86,7 @@ public class Avatar /// 战斗场景 /// public CombatScenes CombatScenes { get; set; } - - public static string? LastActiveAvatar { get; internal set; } = null; - + public Avatar(CombatScenes combatScenes, string name, int index, Rect nameRect, double manualSkillCd = -1) { @@ -115,6 +117,65 @@ public class Avatar Sleep(600, ct); TpForRecover(ct, new RetryException("检测到复苏界面,存在角色被击败,前往七天神像复活")); } + else if(AutoFightParam.SwimmingEnabled && AutoFightTask.FightStatusFlag && SwimmingConfirm(region)) + { + if (AutoFightTask.FightWaypoint is not null) + { + using var ra = CaptureToRectArea(); + if (!SwimmingConfirm(ra)) //二次确认 + { + return; + } + + Logger.LogInformation("游泳检测:尝试回到战斗地点"); + var pathExecutor = new PathExecutor(ct); + try + { + pathExecutor.FaceTo(AutoFightTask.FightWaypoint).Wait(2000, ct); + AutoFightTask.FightWaypoint.MoveMode = MoveModeEnum.Fly.Code;//改为跳飞 + Simulation.SendInput.Mouse.RightButtonDown(); + pathExecutor.MoveTo(AutoFightTask.FightWaypoint).Wait(15000, ct); + AutoFightTask.FightWaypoint = null;//执行后清空,即每次战斗只执行一次,第二次直接去七天神像 + Simulation.SendInput.Mouse.RightButtonUp(); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "游泳检测:回到战斗地点异常"); + } + + Simulation.ReleaseAllKey(); + + using var ra2 = CaptureToRectArea(); + if (!SwimmingConfirm(ra2)) + { + Logger.LogInformation("游泳检测:游泳脱困成功"); + return; + } + + Logger.LogWarning("游泳检测:回到战斗地点失败"); + } + + Logger.LogWarning("战斗过程检测到游泳,前往七天神像重试"); + TpForRecover(ct, new RetryException("战斗过程检测到游泳,前往七天神像重试")); + } + } + + /// + /// 游泳检测(色块连通性检测) + /// 游泳时右下角会出现鼠标图标,带有黄色色块,不受改按键影响 + /// + private static bool SwimmingConfirm(Region region) + { + using var mask = OpenCvCommonHelper.Threshold(region.ToImageRegion().DeriveCrop(1819, 1025, 9, 11).SrcMat, + new Scalar(242, 223, 39),new Scalar(255, 233, 44)); + using var labels = new Mat(); + using var stats = new Mat(); + using var centroids = new Mat(); + + var numLabels = Cv2.ConnectedComponentsWithStats(mask, labels, stats, centroids, + connectivity: PixelConnectivity.Connectivity4, ltype: MatType.CV_32S); + + return numLabels > 1; } /// @@ -138,6 +199,7 @@ public class Avatar /// public void Switch() { + var context = new AvatarActiveCheckContext(); for (var i = 0; i < 30; i++) { if (Ct is { IsCancellationRequested: true }) @@ -145,39 +207,16 @@ public class Avatar return; } - var region = CaptureToRectArea(); + using var region = CaptureToRectArea(); ThrowWhenDefeated(region, Ct); - var notActiveCount = CombatScenes.GetAvatars().Count(avatar => !avatar.IsActive(region)); - if (IsActive(region) && notActiveCount == CombatScenes.ExpectedTeamAvatarNum - 1) + // 切换成功 + if (CombatScenes.GetActiveAvatarIndex(region, context) == Index) { return; } - Simulation.SendInput.SimulateAction(GIActions.Drop); - switch (Index) - { - case 1: - Simulation.SendInput.SimulateAction(GIActions.SwitchMember1); - break; - case 2: - Simulation.SendInput.SimulateAction(GIActions.SwitchMember2); - break; - case 3: - Simulation.SendInput.SimulateAction(GIActions.SwitchMember3); - break; - case 4: - Simulation.SendInput.SimulateAction(GIActions.SwitchMember4); - break; - case 5: - Simulation.SendInput.SimulateAction(GIActions.SwitchMember5); - break; - default: - break; - } - - Offset60Fix(i); - + SimulateSwitchAction(Index); // Debug.WriteLine($"切换到{Index}号位"); // Cv2.ImWrite($"log/切换.png", region.SrcMat); Sleep(250, Ct); @@ -192,6 +231,7 @@ public class Avatar /// public bool TrySwitch(int tryTimes = 4, bool needLog = true) { + var context = new AvatarActiveCheckContext(); for (var i = 0; i < tryTimes; i++) { if (Ct is { IsCancellationRequested: true }) @@ -199,44 +239,22 @@ public class Avatar return false; } - var region = CaptureToRectArea(); + using var region = CaptureToRectArea(); ThrowWhenDefeated(region, Ct); - var notActiveCount = CombatScenes.GetAvatars().Count(avatar => !avatar.IsActive(region)); - if (IsActive(region) && notActiveCount == CombatScenes.ExpectedTeamAvatarNum - 1) + // 切换成功 + if (CombatScenes.GetActiveAvatarIndex(region, context) == Index) { if (needLog && i > 0) { - LastActiveAvatar = Name; Logger.LogInformation("成功切换角色:{Name}", Name); } return true; } - Simulation.SendInput.SimulateAction(GIActions.Drop); //反正会重试就不等落地了 - switch (Index) - { - case 1: - Simulation.SendInput.SimulateAction(GIActions.SwitchMember1); - break; - case 2: - Simulation.SendInput.SimulateAction(GIActions.SwitchMember2); - break; - case 3: - Simulation.SendInput.SimulateAction(GIActions.SwitchMember3); - break; - case 4: - Simulation.SendInput.SimulateAction(GIActions.SwitchMember4); - break; - case 5: - Simulation.SendInput.SimulateAction(GIActions.SwitchMember5); - break; - default: - break; - } - - Offset60Fix(i); + + SimulateSwitchAction(Index); Sleep(250, Ct); } @@ -244,84 +262,54 @@ public class Avatar return false; } + private void SimulateSwitchAction(int index) + { + Simulation.SendInput.SimulateAction(GIActions.Drop); //反正会重试就不等落地了 + switch (index) + { + case 1: + Simulation.SendInput.SimulateAction(GIActions.SwitchMember1); + break; + case 2: + Simulation.SendInput.SimulateAction(GIActions.SwitchMember2); + break; + case 3: + Simulation.SendInput.SimulateAction(GIActions.SwitchMember3); + break; + case 4: + Simulation.SendInput.SimulateAction(GIActions.SwitchMember4); + break; + case 5: + Simulation.SendInput.SimulateAction(GIActions.SwitchMember5); + break; + default: + break; + } + } + /// /// 切换到本角色 /// 切换cd是1秒,如果切换失败,会尝试再次切换,最多尝试5次 /// public void SwitchWithoutCts() { + var context = new AvatarActiveCheckContext(); for (var i = 0; i < 10; i++) { - var region = CaptureToRectArea(); + using var region = CaptureToRectArea(); ThrowWhenDefeated(region, Ct); - var notActiveCount = CombatScenes.GetAvatars().Count(avatar => !avatar.IsActive(region)); - if (IsActive(region) && notActiveCount == 3) + if (CombatScenes.GetActiveAvatarIndex(region, context) == Index) { return; } - Simulation.SendInput.SimulateAction(GIActions.Drop); - switch (Index) - { - case 1: - Simulation.SendInput.SimulateAction(GIActions.SwitchMember1); - break; - case 2: - Simulation.SendInput.SimulateAction(GIActions.SwitchMember2); - break; - case 3: - Simulation.SendInput.SimulateAction(GIActions.SwitchMember3); - break; - case 4: - Simulation.SendInput.SimulateAction(GIActions.SwitchMember4); - break; - case 5: - Simulation.SendInput.SimulateAction(GIActions.SwitchMember5); - break; - default: - break; - } - - Offset60Fix(i); + SimulateSwitchAction(Index); Sleep(250); } } - private void Offset60Fix(int i) - { - // 3次失败考虑是否偏移出现问题,修改偏移位置 - if (i <= 2 || AutoFightTask.FightStatusFlag) - { - return; - } - - if (CombatScenes.IndexRectOffset60Fix) - { - foreach (var avatar in CombatScenes.GetAvatars()) - { - var originalRect = AutoFightAssets.Instance.AvatarIndexRectList[avatar.Index - 1]; - var rect1 = new Rect(originalRect.X, originalRect.Y, originalRect.Width, originalRect.Height); - avatar.IndexRect = rect1; - } - CombatScenes.IndexRectOffset60Fix = false; - } - else - { - foreach (var avatar in CombatScenes.GetAvatars()) - { - var originalRect = AutoFightAssets.Instance.AvatarIndexRectList[avatar.Index - 1]; - var rect1 = new Rect(originalRect.X, originalRect.Y, originalRect.Width, originalRect.Height); - rect1.Y -= 14; - avatar.IndexRect = rect1; - } - - CombatScenes.IndexRectOffset60Fix = true; - } - - } - /// /// 是否出战状态 /// @@ -338,12 +326,12 @@ public class Avatar return !white; } } - + private bool IsIndexRectWhite(ImageRegion region, Rect rect) { // 剪裁出IndexRect区域 - var indexRa = region.DeriveCrop(rect); - var mat = indexRa.CacheGreyMat; + using var indexRa = region.DeriveCrop(rect); + using var mat = indexRa.CacheGreyMat; var count = OpenCvCommonHelper.CountGrayMatColor(mat, 251, 255); if (count * 1.0 / (mat.Width * mat.Height) > 0.5) { @@ -470,7 +458,7 @@ public class Avatar Sleep(200, Ct); - var region = CaptureToRectArea(); + using var region = CaptureToRectArea(); ThrowWhenDefeated(region, Ct); // 检测是不是要跑神像 var cd = AfterUseSkill(region); if (cd > 0) @@ -495,7 +483,7 @@ public class Avatar return GetSkillCdSeconds(); } - var region = givenRegion ?? CaptureToRectArea(); + using var region = givenRegion ?? CaptureToRectArea(); return GetSkillCurrentCd(region); } @@ -506,8 +494,8 @@ public class Avatar /// private double GetSkillCurrentCd(ImageRegion imageRegion) { - var eRa = imageRegion.DeriveCrop(AutoFightAssets.Instance.ECooldownRect); - var eRaWhite = OpenCvCommonHelper.InRangeHsv(eRa.SrcMat, new Scalar(0, 0, 235), new Scalar(0, 25, 255)); + using var eRa = imageRegion.DeriveCrop(AutoFightAssets.Instance.ECooldownRect); + using var eRaWhite = OpenCvCommonHelper.InRangeHsv(eRa.SrcMat, new Scalar(0, 0, 235), new Scalar(0, 25, 255)); var text = OcrFactory.Paddle.OcrWithoutDetector(eRaWhite); var cd = StringUtils.TryParseDouble(text); if (cd > 0 && cd <= CombatAvatar.SkillCd) @@ -525,7 +513,6 @@ public class Avatar /// public void UseBurst() { - // var isBurstReleased = false; for (var i = 0; i < 10; i++) { if (Ct is { IsCancellationRequested: true }) @@ -536,28 +523,15 @@ public class Avatar Simulation.SendInput.SimulateAction(GIActions.ElementalBurst); Sleep(200, Ct); - var region = CaptureToRectArea(); + using var region = CaptureToRectArea(); ThrowWhenDefeated(region, Ct); - var notActiveCount = CombatScenes.GetAvatars().Count(avatar => !avatar.IsActive(region)); - if (notActiveCount == 0) + + if (!PartyAvatarSideIndexHelper.HasAnyIndexRect(region)) { - // isBurstReleased = true; + // 找不到角色编号块意味者技能释放成功 Sleep(1500, Ct); return; } - // else - // { - // if (!isBurstReleased) - // { - // var cd = GetBurstCurrentCd(content); - // if (cd > 0) - // { - // Logger.LogInformation("{Name} 释放元素爆发,cd:{Cd}", Name, cd); - // // todo 把cd加入执行队列 - // return; - // } - // } - // } } } @@ -809,6 +783,7 @@ public class Avatar rateX = lowspeed; rateY = 0; } + Simulation.SendInput.Mouse.MoveMouseBy((int)(rateX * 50 * dpi), (int)(rateY * 50 * dpi)); tick = (tick + 1) % 100; @@ -1014,4 +989,4 @@ public class Avatar return null; } -} +} \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/AutoFight/Model/AvatarActiveCheckContext.cs b/BetterGenshinImpact/GameTask/AutoFight/Model/AvatarActiveCheckContext.cs new file mode 100644 index 00000000..2c3a7829 --- /dev/null +++ b/BetterGenshinImpact/GameTask/AutoFight/Model/AvatarActiveCheckContext.cs @@ -0,0 +1,17 @@ +namespace BetterGenshinImpact.GameTask.AutoFight.Model; + +/// +/// 多次识别出战角色结果上下文 +/// +public class AvatarActiveCheckContext +{ + /// + /// 出战标识识别结果的次数统计 + /// + public int[] ActiveIndexByArrowCount { get; set; } = new int[4]; + + /// + /// 累计识别失败次数 + /// + public int TotalCheckFailedCount { get; set; } = 0; +} \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/AutoFight/Model/CombatScenes.cs b/BetterGenshinImpact/GameTask/AutoFight/Model/CombatScenes.cs index 856e53b8..f608ec70 100644 --- a/BetterGenshinImpact/GameTask/AutoFight/Model/CombatScenes.cs +++ b/BetterGenshinImpact/GameTask/AutoFight/Model/CombatScenes.cs @@ -1,25 +1,28 @@ -using BetterGenshinImpact.Core.Recognition.OCR; +using BetterGenshinImpact.Core.Config; +using BetterGenshinImpact.Core.Recognition.OCR; using BetterGenshinImpact.Core.Recognition.ONNX; using BetterGenshinImpact.Core.Recognition.OpenCv; +using BetterGenshinImpact.Core.Simulator; using BetterGenshinImpact.GameTask.AutoFight.Assets; using BetterGenshinImpact.GameTask.AutoFight.Config; +using BetterGenshinImpact.GameTask.Common; +using BetterGenshinImpact.GameTask.Common.Element.Assets; +using BetterGenshinImpact.GameTask.Model; using BetterGenshinImpact.GameTask.Model.Area; using BetterGenshinImpact.Helpers; +using Compunet.YoloSharp; +using Compunet.YoloSharp.Data; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using OpenCvSharp; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; using System.Threading; -using BetterGenshinImpact.Core.Simulator; -using Compunet.YoloSharp; -using Compunet.YoloSharp.Data; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; -using static BetterGenshinImpact.GameTask.Common.TaskControl; -using Microsoft.Extensions.DependencyInjection; namespace BetterGenshinImpact.GameTask.AutoFight.Model; @@ -35,17 +38,71 @@ public class CombatScenes : IDisposable public int AvatarCount => Avatars.Length; + /// + /// 最近一次识别出的出战角色编号,从1开始,-1表示未识别 + /// + public int LastActiveAvatarIndex { get; set; } = -1; - private readonly BgiYoloPredictor _predictor = - App.ServiceProvider.GetRequiredService().CreateYoloPredictor(BgiOnnxModel.BgiAvatarSide); + public MultiGameStatus? CurrentMultiGameStatus { set; get; } + + private readonly BgiYoloPredictor _predictor; + private readonly bool _ownsPredictor; + + private readonly AutoFightAssets _autoFightAssets; + + private readonly ElementAssets _elementAssets; + + private readonly ILogger _logger; + + private readonly ISystemInfo _systemInfo; + + public CombatScenes(BgiYoloPredictor? predictor = null, AutoFightAssets? autoFightAssets = null, ILogger? logger = null, ElementAssets? elementAssets = null, ISystemInfo? systemInfo = null) + { + if (predictor == null) + { + _predictor = App.ServiceProvider.GetRequiredService().CreateYoloPredictor(BgiOnnxModel.BgiAvatarSide); + _ownsPredictor = true; + } + else + { + _predictor = predictor; + _ownsPredictor = false; + } + if (autoFightAssets == null) + { + _autoFightAssets = AutoFightAssets.Instance; // todo BaseAssets重构后直接由systemInfo构建,省去传入? + } + else + { + _autoFightAssets = autoFightAssets; + } + if (logger == null) + { + _logger = TaskControl.Logger; + } + else + { + _logger = logger; + } + if (elementAssets == null) + { + _elementAssets = ElementAssets.Instance; + } + else + { + _elementAssets = elementAssets; + } + if (systemInfo == null) + { + _systemInfo = TaskContext.Instance().SystemInfo; + } + else + { + _systemInfo = systemInfo; + } + } public int ExpectedTeamAvatarNum { get; private set; } = 4; - - - /// - /// 6.0 UI偏移标识 - /// - public bool IndexRectOffset60Fix { get; set; } /// /// 获取一个只读的Avatars @@ -61,54 +118,27 @@ public class CombatScenes : IDisposable /// 通过YOLO分类器识别队伍内角色 /// /// 完整游戏画面的捕获截图 - public CombatScenes InitializeTeam(ImageRegion imageRegion) + public CombatScenes InitializeTeam(ImageRegion imageRegion, AutoFightConfig? autoFightConfig = null) { + if (autoFightConfig == null) + { + autoFightConfig = TaskContext.Instance().Config.AutoFightConfig; + } + AssertUtils.CheckGameResolution(); // 优先取配置 - if (!string.IsNullOrEmpty(TaskContext.Instance().Config.AutoFightConfig.TeamNames)) + if (!string.IsNullOrEmpty(autoFightConfig.TeamNames)) { - InitializeTeamFromConfig(TaskContext.Instance().Config.AutoFightConfig.TeamNames); + InitializeTeamFromConfig(autoFightConfig.TeamNames, autoFightConfig); return this; } - // 判断当前是否处于联机状态 - List avatarSideIconRectList; - List avatarIndexRectList; - var pRaList = imageRegion.FindMulti(AutoFightAssets.Instance.PRa); - if (pRaList.Count > 0) - { - var num = pRaList.Count + 1; - if (num > 4) - { - throw new Exception("当前处于联机状态,但是队伍人数超过4人,无法识别"); - } - // 联机状态下判断 - var onePRa = imageRegion.Find(AutoFightAssets.Instance.OnePRa); - var p = "p"; - if (!onePRa.IsEmpty()) - { - Logger.LogInformation("当前处于联机状态,且当前账号是房主,联机人数{Num}人", num); - p = "1p"; - } - else - { - Logger.LogInformation("当前处于联机状态,且在别人世界中,联机人数{Num}人", num); - } - - avatarSideIconRectList = new List(AutoFightAssets.Instance.AvatarSideIconRectListMap[$"{p}_{num}"]); - avatarIndexRectList = new List(AutoFightAssets.Instance.AvatarIndexRectListMap[$"{p}_{num}"]); - - ExpectedTeamAvatarNum = avatarSideIconRectList.Count; - } - else - { - avatarSideIconRectList = new List(AutoFightAssets.Instance.AvatarSideIconRectList); - avatarIndexRectList = new List(AutoFightAssets.Instance.AvatarIndexRectList); - } - - // 6.0 版本 队伍下的 草露 进度条 导致位置偏移 - IndexRectOffset60Fix = AvatarSideFixOffset(imageRegion, avatarSideIconRectList, avatarIndexRectList); + // 判断联机状态 + CurrentMultiGameStatus = PartyAvatarSideIndexHelper.DetectedMultiGameStatus(imageRegion, _autoFightAssets, _logger); + // 队伍角色编号和侧面头像位置 + var (avatarIndexRectList, avatarSideIconRectList) = PartyAvatarSideIndexHelper.GetAllIndexRects(imageRegion, CurrentMultiGameStatus, _logger, _elementAssets, _systemInfo); + ExpectedTeamAvatarNum = avatarIndexRectList.Count; // 识别队伍 var names = new string[avatarSideIconRectList.Count]; @@ -117,13 +147,13 @@ public class CombatScenes : IDisposable { for (var i = 0; i < avatarSideIconRectList.Count; i++) { - var ra = imageRegion.DeriveCrop(avatarSideIconRectList[i]); + using var ra = imageRegion.DeriveCrop(avatarSideIconRectList[i]); var pair = ClassifyAvatarCnName(ra.CacheImage, i + 1); names[i] = pair.Item1; if (!string.IsNullOrEmpty(pair.Item2)) { var costumeName = pair.Item2; - if (AutoFightAssets.Instance.AvatarCostumeMap.TryGetValue(costumeName, out string? name)) + if (_autoFightAssets.AvatarCostumeMap.TryGetValue(costumeName, out string? name)) { costumeName = name; } @@ -136,64 +166,64 @@ public class CombatScenes : IDisposable } } - Logger.LogInformation("识别到的队伍角色:{Text}", string.Join(",", displayNames)); - Avatars = BuildAvatars([.. names], null, avatarIndexRectList); + _logger.LogInformation("识别到的队伍角色:{Text}", string.Join(",", displayNames)); + Avatars = BuildAvatars([.. names], null, avatarIndexRectList, autoFightConfig); } - catch (Exception e) + catch (Exception e) // todo 此处catch把错误吞了不便排查 { - Logger.LogWarning(e.Message); + _logger.LogWarning(e.Message); } return this; } - + + /// - /// 6.0 版本 队伍下的 草露 进度条 导致位置偏移 - /// + /// 这个个方法主要用于在切人判断有误的情况下,且能够找到预期数量的角色编号框。此时只有两种情况 + /// 1. A草露进度条导致角色编号框偏移,B退队后偏移不变,C独立地图传送后偏移还原 + /// 2. 地图边缘环境,导致角色编号框切人判断失效 + /// 此方法必须在判定一定存在 ExpectedTeamAvatarNum 数量的 IndexRectList 的情况下才能使用 /// /// - /// - /// - public bool AvatarSideFixOffset(ImageRegion imageRegion, List avatarSideIconRectList, List avatarIndexRectList) + /// false:存在 IndexRectList 的情况下使用此方法,返回false的时候很有可能处于地图边缘环境下 + public bool RefreshTeamAvatarIndexRectList(ImageRegion imageRegion) { - // 角色序号 左上角 坐标偏移(+2, -5)后存在3个白色点,则认为存在 草露 进度条 - // 存在 草露 进度条时候整体上移 14 个像素 - var whitePointCount = 0; - foreach (var rectIndex in avatarIndexRectList) + // 只用新方法判断 + try { - int x = rectIndex.X + 2; - int y = rectIndex.Y - 5; - var color = imageRegion.SrcMat.At(y, x); - if (color is { Item0: 255, Item1: 255, Item2: 255 }) + var (avatarIndexRectList, _) = PartyAvatarSideIndexHelper.GetAllIndexRectsNew(imageRegion, CurrentMultiGameStatus!, _logger, _elementAssets, _systemInfo); + if (avatarIndexRectList.Count != ExpectedTeamAvatarNum) { - whitePointCount++; + _logger.LogWarning("重新识别到的队伍角色数量与之前不一致,之前{Old}个,现在{New}个", ExpectedTeamAvatarNum, avatarIndexRectList.Count); + return false; } - } - if (whitePointCount < 3) + for (var i = 0; i < ExpectedTeamAvatarNum; i++) + { + Avatars[i].IndexRect = avatarIndexRectList[i]; + } + + return true; + } + catch (Exception ex) { + _logger.LogDebug(ex, "使用新方法获取角色编号位置失败"); + _logger.LogWarning("[重新识别角色编号位置]使用新方法获取角色编号位置失败,原因:" + ex.Message); return false; } - - Logger.LogInformation("检测到右侧队伍上偏移,进行位置偏移"); - - for (var i = 0; i < avatarSideIconRectList.Count; i++) - { - var rect = avatarSideIconRectList[i]; - rect.Y -= 14; - avatarSideIconRectList[i] = rect; - } - - for (var i = 0; i < avatarIndexRectList.Count; i++) - { - var rect = avatarIndexRectList[i]; - rect.Y -= 14; - avatarIndexRectList[i] = rect; - } - - return true; } - + + // public static List FindAvatarIndexRectList(ImageRegion imageRegion) + // { + // var i1 = imageRegion.Find(ElementAssets.Instance.Index1); + // var i2 = imageRegion.Find(ElementAssets.Instance.Index2); + // var i3 = imageRegion.Find(ElementAssets.Instance.Index3); + // var i4 = imageRegion.Find(ElementAssets.Instance.Index4); + // var curr = imageRegion.Find(ElementAssets.Instance.CurrentAvatarThreshold); + // // Debug.WriteLine($"i1:{i1.X},{i1.Y},{i1.Width},{i1.Height}; i2:{i2.X},{i2.Y},{i2.Width},{i2.Height}; i3:{i3.X},{i3.Y},{i3.Width},{i3.Height}; i4:{i4.X},{i4.Y},{i4.Width},{i4.Height}; curr:{curr.X},{curr.Y},{curr.Width},{curr.Height}"); + // return null; + // } + public (string, string) ClassifyAvatarCnName(Image img, int index) { @@ -226,7 +256,7 @@ public class CombatScenes : IDisposable // 降低琴和衣装角色的识别率要求 if (topClass.Confidence < 0.51) { - img.SaveAsPng(@"log\avatar_side_classify_error.png"); + img.SaveAsPng(Global.Absolute($@"log\avatar_side_classify_error.png")); throw new Exception( $"无法识别第{index}位角色,置信度{topClass.Confidence:F1},结果:{topClass.Name.Name}。请重新阅读 BetterGI 文档中的《快速上手》!"); } @@ -235,7 +265,7 @@ public class CombatScenes : IDisposable { if (topClass.Confidence < 0.7) { - img.SaveAsPng(@"log\avatar_side_classify_error.png"); + img.SaveAsPng(Global.Absolute($@"log\avatar_side_classify_error.png")); throw new Exception( $"无法识别第{index}位角色,置信度{topClass.Confidence:F1},结果:{topClass.Name.Name}。请重新阅读 BetterGI 文档中的《快速上手》!"); } @@ -244,7 +274,7 @@ public class CombatScenes : IDisposable return topClass.Name.Name; } - private void InitializeTeamFromConfig(string teamNames) + private void InitializeTeamFromConfig(string teamNames, AutoFightConfig autoFightConfig) { var names = teamNames.Split([",", ","], StringSplitOptions.TrimEntries); if (names.Length != 4) @@ -258,9 +288,9 @@ public class CombatScenes : IDisposable names[i] = DefaultAutoFightConfig.AvatarAliasToStandardName(names[i]); } - Logger.LogInformation("强制指定队伍角色:{Text}", string.Join(",", names)); - TaskContext.Instance().Config.AutoFightConfig.TeamNames = string.Join(",", names); - Avatars = BuildAvatars([.. names]); + _logger.LogInformation("强制指定队伍角色:{Text}", string.Join(",", names)); + autoFightConfig.TeamNames = string.Join(",", names); + Avatars = BuildAvatars([.. names], autoFightConfig: autoFightConfig); } public bool CheckTeamInitialized() @@ -274,13 +304,16 @@ public class CombatScenes : IDisposable } - private Avatar[] BuildAvatars(List names, List? nameRects = null, - List? avatarIndexRectList = null) + private Avatar[] BuildAvatars(List names, List? nameRects = null, List? avatarIndexRectList = null, AutoFightConfig? autoFightConfig = null) { - var cdConfig = TaskContext.Instance().Config.AutoFightConfig.ActionSchedulerByCd; + if (autoFightConfig == null) + { + autoFightConfig = TaskContext.Instance().Config.AutoFightConfig; + } + var cdConfig = autoFightConfig.ActionSchedulerByCd; if (avatarIndexRectList == null && ExpectedTeamAvatarNum == 4) { - avatarIndexRectList = AutoFightAssets.Instance.AvatarIndexRectList; + avatarIndexRectList = _autoFightAssets.AvatarIndexRectList; } if (avatarIndexRectList == null) @@ -369,7 +402,7 @@ public class CombatScenes : IDisposable { if (avatarIndex < 1 || avatarIndex > AvatarCount) { - Logger.LogError("切换角色编号错误,当前角色数量{Count},编号{Index}", AvatarCount, avatarIndex); + _logger.LogError("切换角色编号错误,当前角色数量{Count},编号{Index}", AvatarCount, avatarIndex); throw new Exception("不存在的角色编号"); } @@ -378,6 +411,8 @@ public class CombatScenes : IDisposable /// /// 获取当前出战角色名 + /// 不考虑重新刷新编号框位置 + /// 不推荐使用 /// /// /// @@ -386,30 +421,63 @@ public class CombatScenes : IDisposable public string? CurrentAvatar(bool force = false, ImageRegion? region = null, CancellationToken ct = default) { - if (!force && Avatar.LastActiveAvatar is not null) + if (!force && LastActiveAvatarIndex > 0) { - return Avatar.LastActiveAvatar; + return Avatars[LastActiveAvatarIndex - 1].Name; } - var imageRegion = region ?? CaptureToRectArea(); - string? avatarName = null; + using var imageRegion = region ?? TaskControl.CaptureToRectArea(); - var notActiveCount = 0; - foreach (var avatar in GetAvatars()) + var rectArray = Avatars.Select(t => t.IndexRect).ToArray(); + int index = PartyAvatarSideIndexHelper.GetAvatarIndexIsActiveWithContext(imageRegion, rectArray, new AvatarActiveCheckContext()); + + if (index > 0) { - if (avatar.IsActive(imageRegion)) + LastActiveAvatarIndex = index; + } + + return Avatars[LastActiveAvatarIndex - 1].Name; + } + + /// + /// 推荐使用 + /// 失败后自动刷新编号框位置 + /// + /// l + /// + /// + public int GetActiveAvatarIndex(ImageRegion imageRegion, AvatarActiveCheckContext context) + { + var rectArray = Avatars.Select(t => t.IndexRect).ToArray(); + int index = PartyAvatarSideIndexHelper.GetAvatarIndexIsActiveWithContext(imageRegion, rectArray, context); + + if (index > 0) + { + LastActiveAvatarIndex = index; + return index; + } + else + { + // 多次识别失败则尝试刷新角色编号位置 + // 应对草露问题 + if (context.TotalCheckFailedCount % 3 == 0 && context.TotalCheckFailedCount > 0 && context.TotalCheckFailedCount < 10) { - avatarName = avatar.Name; - } - else - { - notActiveCount++; + // 失败多次,识别是否存在满足预期的编号框 + if (PartyAvatarSideIndexHelper.CountIndexRect(imageRegion) == Avatars.Length) + { + bool res = RefreshTeamAvatarIndexRectList(imageRegion); + _logger.LogWarning("多次识别出战角色失败,尝试刷新角色编号位置(处理草露问题),刷新结果:{Result}", res ? "成功" : "失败"); + imageRegion.SrcMat.SaveImage(Global.Absolute("log\\refresh_avatar_index_rect.png")); + if (res) + { + context.TotalCheckFailedCount = 0; + } + } } } - if (notActiveCount != ExpectedTeamAvatarNum - 1) return avatarName; - Avatar.LastActiveAvatar = avatarName; - return Avatar.LastActiveAvatar; + + return -1; } @@ -425,12 +493,12 @@ public class CombatScenes : IDisposable // 优先取配置 if (!string.IsNullOrEmpty(TaskContext.Instance().Config.AutoFightConfig.TeamNames)) { - InitializeTeamFromConfig(TaskContext.Instance().Config.AutoFightConfig.TeamNames); + InitializeTeamFromConfig(TaskContext.Instance().Config.AutoFightConfig.TeamNames, TaskContext.Instance().Config.AutoFightConfig); return this; } // 剪裁出队伍区域 - var teamRa = content.CaptureRectArea.DeriveCrop(AutoFightAssets.Instance.TeamRectNoIndex); + var teamRa = content.CaptureRectArea.DeriveCrop(_autoFightAssets.TeamRectNoIndex); // 过滤出白色 var hsvFilterMat = OpenCvCommonHelper.InRangeHsv(teamRa.SrcMat, new Scalar(0, 0, 210), new Scalar(255, 30, 255)); @@ -459,23 +527,23 @@ public class CombatScenes : IDisposable if (names.Count != 4) { - Logger.LogWarning("识别到的队伍角色数量不正确,当前识别结果:{Text}", string.Join(",", names)); + _logger.LogWarning("识别到的队伍角色数量不正确,当前识别结果:{Text}", string.Join(",", names)); } if (names.Count == 3) { // 流浪者特殊处理 // 4人以上的队伍,不支持流浪者的识别 - var wanderer = rectArea.Find(AutoFightAssets.Instance.WandererIconRa); + var wanderer = rectArea.Find(_autoFightAssets.WandererIconRa); if (wanderer.IsEmpty()) { - wanderer = rectArea.Find(AutoFightAssets.Instance.WandererIconNoActiveRa); + wanderer = rectArea.Find(_autoFightAssets.WandererIconNoActiveRa); } if (wanderer.IsEmpty()) { // 补充识别流浪者 - Logger.LogWarning("二次尝试识别失败,当前识别结果:{Text}", string.Join(",", names)); + _logger.LogWarning("二次尝试识别失败,当前识别结果:{Text}", string.Join(",", names)); } else { @@ -501,12 +569,12 @@ public class CombatScenes : IDisposable if (names.Count != 4) { - Logger.LogWarning("图像识别到流浪者,但识别队内位置信息失败"); + _logger.LogWarning("图像识别到流浪者,但识别队内位置信息失败"); } } } - Logger.LogInformation("识别到的队伍角色:{Text}", string.Join(",", names)); + _logger.LogInformation("识别到的队伍角色:{Text}", string.Join(",", names)); Avatars = BuildAvatars(names, nameRects); } @@ -542,6 +610,9 @@ public class CombatScenes : IDisposable public void Dispose() { - _predictor.Dispose(); + if (_ownsPredictor) + { + _predictor.Dispose(); + } } -} +} \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/AutoFight/Model/MultiGameStatus.cs b/BetterGenshinImpact/GameTask/AutoFight/Model/MultiGameStatus.cs new file mode 100644 index 00000000..31859803 --- /dev/null +++ b/BetterGenshinImpact/GameTask/AutoFight/Model/MultiGameStatus.cs @@ -0,0 +1,68 @@ +using System; +using System.Threading; +using OpenCvSharp; + +namespace BetterGenshinImpact.GameTask.AutoFight.Model; + +public class MultiGameStatus +{ + /// + /// 是否在联机状态 + /// + public bool IsInMultiGame { get; set; } = false; + + /// + /// 是不是房主 + /// 我是房主的情况下 + /// 1人联机:最多控制4名角色 + /// 2人联机:最多控制2名角色 + /// 3人联机:最多控制2名角色 + /// 4人联机:最多控制1名角色 + /// 我不是房主的情况下 + /// 2人联机:最多控制2名角色 + /// 3人联机:最多控制1名角色 + /// 4人联机:最多控制1名角色 + /// + public bool IsHost { get; set; } = false; + + /// + /// 玩家数量,最少1人(我自己) + /// + public int PlayerCount { get; set; } = 1; + + /// + /// 我能控制的最大角色数量 + /// + public int MaxControlAvatarCount + { + get + { + if (!IsInMultiGame) + { + return 4; + } + + if (IsHost) + { + return PlayerCount switch + { + 1 => 4, + 2 => 2, + 3 => 2, + 4 => 1, + _ => throw new ArgumentOutOfRangeException(nameof(PlayerCount), "自己为主机时,联机总人数异常") + }; + } + else + { + return PlayerCount switch + { + 2 => 2, + 3 => 1, + 4 => 1, + _ => throw new ArgumentOutOfRangeException(nameof(PlayerCount), "进入别人世界时,联机总人数异常") + }; + } + } + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/AutoFight/Model/PartyAvatarSideIndexHelper.cs b/BetterGenshinImpact/GameTask/AutoFight/Model/PartyAvatarSideIndexHelper.cs new file mode 100644 index 00000000..a600698e --- /dev/null +++ b/BetterGenshinImpact/GameTask/AutoFight/Model/PartyAvatarSideIndexHelper.cs @@ -0,0 +1,457 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using BetterGenshinImpact.Core.Recognition.OpenCv; +using BetterGenshinImpact.GameTask.AutoFight.Assets; +using BetterGenshinImpact.GameTask.Common; +using BetterGenshinImpact.GameTask.Common.Element.Assets; +using BetterGenshinImpact.GameTask.Model; +using BetterGenshinImpact.GameTask.Model.Area; +using Microsoft.Extensions.Logging; +using OpenCvSharp; + +namespace BetterGenshinImpact.GameTask.AutoFight.Model; + +/// +/// 用于处理主界面右侧角色编号的一些方法 +/// +public class PartyAvatarSideIndexHelper +{ + /// + /// 角色编号以当前模板匹配结果的情况下的Y轴公差 + /// + private static readonly int IndexRectDistanceY = 96; + + /// + /// 检查当前联机状态 + /// + /// + /// + /// + public static MultiGameStatus DetectedMultiGameStatus(ImageRegion imageRegion, AutoFightAssets? autoFightAssets = null, ILogger? logger = null) + { + if (autoFightAssets == null) + { + autoFightAssets = AutoFightAssets.Instance; + } + if (logger == null) + { + logger = TaskControl.Logger; + } + var status = new MultiGameStatus(); + // 判断当前联机人数 + var pRaList = imageRegion.FindMulti(autoFightAssets.PRa); + if (pRaList.Count > 0) + { + status.IsInMultiGame = true; + var num = pRaList.Count + 1; + if (num > 4) + { + throw new Exception("当前处于联机状态,但是队伍人数超过4人,无法识别"); + } + + status.PlayerCount = num; + + // 联机状态下判断 + var onePRa = imageRegion.Find(autoFightAssets.OnePRa); + if (onePRa.IsExist()) + { + logger.LogInformation("当前处于联机状态,且当前账号是房主,联机人数{Num}人", num); + status.IsHost = true; + } + else + { + logger.LogInformation("当前处于联机状态,且在别人世界中,联机人数{Num}人", num); + } + } + else + { + // 没有其他联机玩家的情况下,也有可能是单人房主 + var onePRa = imageRegion.Find(autoFightAssets.OnePRa); + if (onePRa.IsExist()) + { + logger.LogInformation("当前处于联机状态,但是没有其他玩家连入"); + status.IsInMultiGame = true; + status.IsHost = true; + status.PlayerCount = 1; + } + } + + return status; + } + + /// + /// 根据已知的某个角色编号位置,计算其他角色编号的位置 + /// + /// 已知编号 + /// 已知编号矩形 + /// 目标编号 + /// 目标编号矩形 + public static Rect GetIndexRectFromKnownIndexRect(int knownIndex, Rect knownRect, int targetIndex) + { + var s = TaskContext.Instance().SystemInfo.AssetScale; + + // y_k + (n - k) * d + int y = knownRect.Y + (targetIndex - knownIndex) * (int)(IndexRectDistanceY * s); + + return new Rect(knownRect.X, y, knownRect.Width, knownRect.Height); + } + + public static Rect GetIndexRectFromKnownCurrentAvatarFlag(Rect currRect) + { + var s = TaskContext.Instance().SystemInfo.AssetScale; + return new Rect(currRect.X + (int)(126 * s), currRect.Y - (int)(194 * s), (int)(16 * s), (int)(17 * s)); + } + + public static (List, List) GetAllIndexRects(ImageRegion imageRegion, MultiGameStatus multiGameStatus, ILogger logger, ElementAssets elementAssets, ISystemInfo systemInfo) + { + try + { + // 新的动态获取角色编号位置逻辑 + return GetAllIndexRectsNew(imageRegion, multiGameStatus, logger, elementAssets, systemInfo); + } + catch (Exception ex) + { + logger.LogDebug(ex, "使用新方法获取角色编号位置失败"); + logger.LogWarning("使用新方法获取角色编号位置失败,原因:" + ex.Message); + logger.LogWarning("尝试使用旧的写死位置逻辑"); + // 旧的写死位置逻辑 + return GetAllIndexRectsOld(imageRegion, multiGameStatus); + } + } + + private static (List, List) GetAllIndexRectsOld(ImageRegion imageRegion, MultiGameStatus multiGameStatus) + { + List avatarSideIconRectList; + List avatarIndexRectList; + if (multiGameStatus.IsInMultiGame) + { + var p = multiGameStatus.IsHost ? "1p" : "p"; + avatarSideIconRectList = new List(AutoFightAssets.Instance.AvatarSideIconRectListMap[$"{p}_{multiGameStatus.PlayerCount}"]); + avatarIndexRectList = new List(AutoFightAssets.Instance.AvatarIndexRectListMap[$"{p}_{multiGameStatus.PlayerCount}"]); + } + else + { + avatarSideIconRectList = new List(AutoFightAssets.Instance.AvatarSideIconRectList); + avatarIndexRectList = new List(AutoFightAssets.Instance.AvatarIndexRectList); + } + + // 6.0 版本 队伍下的 草露 进度条 导致位置偏移 + AvatarSideFixOffset(imageRegion, avatarSideIconRectList, avatarIndexRectList); + return (avatarIndexRectList, avatarSideIconRectList); + } + + public static bool HasAnyIndexRect(ImageRegion imageRegion) + { + return ElementAssets.Instance.IndexList.Select(indexRo => imageRegion.Find(indexRo)).Any(indexRes => indexRes.IsExist()); + } + + public static int CountIndexRect(ImageRegion imageRegion) + { + return ElementAssets.Instance.IndexList.Select(indexRo => imageRegion.Find(indexRo)).Count(indexRes => indexRes.IsExist()); + } + + public static bool HasActiveAvatarArrow(ImageRegion imageRegion) + { + return imageRegion.Find(ElementAssets.Instance.CurrentAvatarThreshold).IsExist(); + } + + public static (List, List) GetAllIndexRectsNew(ImageRegion imageRegion, MultiGameStatus multiGameStatus, ILogger logger, ElementAssets elementAssets, ISystemInfo systemInfo) + { + // 找到编号块 + var i1 = imageRegion.Find(elementAssets.Index1); + var i2 = imageRegion.Find(elementAssets.Index2); + var i3 = imageRegion.Find(elementAssets.Index3); + var i4 = imageRegion.Find(elementAssets.Index4); + List indexRectList = [i1.ToRect(), i2.ToRect(), i3.ToRect(), i4.ToRect()]; + int existNum = indexRectList.Count(indexRect => indexRect != default); + if (existNum == multiGameStatus.MaxControlAvatarCount) + { + // 识别存在个数和当前能控制的最大角色数相等,意味者全部识别,直接返回 + var notNullIndexRectList = indexRectList.Where(r => r != default).ToList(); + return (notNullIndexRectList, GetAvatarSideIconRectFromIndexRect(notNullIndexRectList, systemInfo)); + } + else + { + // 为什么这里要用箭头确认一遍?因为出战角色编号框的识别率不是100%,需要用箭头来辅助确认。这也是为了保证非满队情况下的队伍识别率 + // 非出战角色编号框识别率100% + var curr = imageRegion.Find(elementAssets.CurrentAvatarThreshold); // 当前出战角色标识 + if (curr.IsExist()) + { + var (knownIndex, knownRect) = GetKnownIndexAndRect(indexRectList); + if (knownRect == default) + { + // 没有已知的编号位置,这种情况下可能是单人队 + // 直接用出战角色标识来反推 + var oneIndexRect = GetIndexRectFromKnownCurrentAvatarFlag(curr.ToRect()); + logger.LogInformation("当前编队中可能只存在一个角色(且角色编号未正确识别)"); + return ([oneIndexRect], [GetAvatarSideIconRectFromIndexRect(oneIndexRect, systemInfo)]); + } + else + { + // 有已知的编号位置,通过已知位置来推测其他位置 + for (int i = 0; i < indexRectList.Count; i++) + { + if (indexRectList[i] == default) + { + var rect = GetIndexRectFromKnownIndexRect(knownIndex, knownRect, i + 1); + if (IsIntersecting(curr.Y, curr.Height, rect.Y, rect.Height)) + { + // 如果和当前出战角色标识相交,说明这个位置是正确的 + indexRectList[i] = rect; + logger.LogInformation("当前出战角色未正确识别,通过出战标识推测角色编号为{Index}", i + 1); + } + } + } + + // 校验推测结果(编号从 1 开始必定连续) + if (AreNullsAtEnd(indexRectList)) + { + var notNullIndexRectList = indexRectList.Where(r => r != default).ToList(); + return (notNullIndexRectList, GetAvatarSideIconRectFromIndexRect(notNullIndexRectList, systemInfo)); + } + else + { + throw new Exception("校验角色列表识别结果失败,角色编号不是连续的!"); + } + } + } + else + { + // 没有出战角色标识的情况下,直接抛出错误走写死逻辑 + throw new Exception("找不到出战角色编号块与当前出战角色标识!"); + } + } + } + + private static (int, Rect) GetKnownIndexAndRect(List indexRectList) + { + for (int i = 0; i < indexRectList.Count; i++) + { + if (indexRectList[i] != default) + { + return (i + 1, indexRectList[i]); + } + } + + return (-1, default); + } + + public static Rect GetAvatarSideIconRectFromIndexRect(Rect indexRect, ISystemInfo systemInfo) + { + var s = systemInfo.AssetScale; + return new Rect(indexRect.X - (int)(91 * s), indexRect.Y - (int)(47 * s), (int)(82 * s), (int)(82 * s)); + } + + public static List GetAvatarSideIconRectFromIndexRect(List indexRect, ISystemInfo systemInfo) + { + return indexRect.Select(r=> GetAvatarSideIconRectFromIndexRect(r, systemInfo)).ToList(); + } + + public static bool IsIntersecting(double y1, double h1, double y2, double h2) + { + // 计算第一个区域的结束位置 + double end1 = y1 + h1; + // 计算第二个区域的结束位置 + double end2 = y2 + h2; + return y1 < end2 && y2 < end1; + } + + public static bool AreNullsAtEnd(List list) + { + int firstNullIndex = list.FindIndex(x => x == default); // 找到第一个 null 的索引 + return firstNullIndex == -1 || list.Skip(firstNullIndex).All(x => x == default); // 检查从第一个 null 开始到末尾是否都是 null + } + + /// + /// 6.0 版本 队伍下的 草露 进度条 导致位置偏移 + /// + /// + /// + /// + /// + public static bool AvatarSideFixOffset(ImageRegion imageRegion, List avatarSideIconRectList, List avatarIndexRectList) + { + // 角色序号 左上角 坐标偏移(+2, -5)后存在3个白色点,则认为存在 草露 进度条 + // 存在 草露 进度条时候整体上移 14 个像素 + var whitePointCount = 0; + foreach (var rectIndex in avatarIndexRectList) + { + int x = rectIndex.X + 2; + int y = rectIndex.Y - 5; + var color = imageRegion.SrcMat.At(y, x); + if (color is { Item0: 255, Item1: 255, Item2: 255 }) + { + whitePointCount++; + } + } + + if (whitePointCount < 3) + { + return false; + } + + TaskControl.Logger.LogInformation("检测到右侧队伍上偏移,进行位置偏移"); + + for (var i = 0; i < avatarSideIconRectList.Count; i++) + { + var rect = avatarSideIconRectList[i]; + rect.Y -= 14; + avatarSideIconRectList[i] = rect; + } + + for (var i = 0; i < avatarIndexRectList.Count; i++) + { + var rect = avatarIndexRectList[i]; + rect.Y -= 14; + avatarIndexRectList[i] = rect; + } + + return true; + } + + /// + /// 识别当前出战角色编号 + /// 1. 颜色识别只要成功一次就认为成功并返回(优先级最高) + /// 2. 出战标识识别成功,颜色识别失败,认为结果不确定,需要重试一次。2次后结果相同认为成功 + /// + /// + /// + /// + /// + public static int GetAvatarIndexIsActiveWithContext(ImageRegion imageRegion, Rect[] rectArray, AvatarActiveCheckContext context) + { + var indexByColor = FindActiveIndexRectByColor(imageRegion, rectArray); + if (indexByColor > 0) + { + context.TotalCheckFailedCount = 0; + return indexByColor; + } + + var indexByArrow = FindActiveIndexRectByArrow(imageRegion, rectArray); + if (indexByArrow > 0) + { + // 累计识别次数 + context.ActiveIndexByArrowCount[indexByArrow - 1]++; + if (context.ActiveIndexByArrowCount[indexByArrow - 1] >= 2) + { + context.TotalCheckFailedCount = 0; + return indexByArrow; + } + + return -2; // 重试 + } + + context.TotalCheckFailedCount++; + return -1; // 两种方式都失败 + } + + // public static int FindDifferentRect(Mat greyMat, Rect[] rectArray) + // { + // // 取其中一个矩形和另外三个矩形进行比较 + // var one = new Mat(greyMat, rectArray[0]); + // for (int i = 1; i < rectArray.Length; i++) + // { + // Mat diff = new Mat(); + // Cv2.Absdiff(one, new Mat(greyMat, rectArray[i]), diff); + // Scalar diffSum = Cv2.Sum(diff); + // double totalDiff = diffSum.Val0 + diffSum.Val1 + diffSum.Val2; + // totalDiff = totalDiff / (one.Width * one.Height); + // } + // + // return 1; + // } + + public static int FindActiveIndexRectByColor(ImageRegion imageRegion, Rect[] rectArray) + { + if (rectArray.Length == 1) + { + return 1; + } + + Mat[] mats = new Mat[rectArray.Length]; + try + { + var whiteCount = 0; + var notWhiteRectNum = 0; + var mat = imageRegion.CacheGreyMat; + for (int i = 0; i < rectArray.Length; i++) + { + var indexMat = new Mat(mat, rectArray[i]); + mats[i] = indexMat; + if (IsWhiteRect(indexMat)) + { + whiteCount++; + } + else + { + notWhiteRectNum = i + 1; + } + } + + if (whiteCount == rectArray.Length - 1) + { + return notWhiteRectNum; + } + else + { + // 使用更加靠谱的差值识别(-1是未识别) + return ImageDifferenceDetector.FindMostDifferentImage(mats); + } + } + finally + { + foreach (var mat in mats) + { + mat?.Dispose(); + } + } + + } + + public static bool IsWhiteRect(Mat indexMat) + { + + var count1 = OpenCvCommonHelper.CountGrayMatColor(indexMat, 251, 255); // 白 + var count2 = OpenCvCommonHelper.CountGrayMatColor(indexMat, 50, 54); // 黑色文字 + if ((count1 + count2) * 1.0 / (indexMat.Width * indexMat.Height) > 0.4) + { + // Debug.WriteLine($"白色矩形占比{(count1 + count2) * 1.0 / (indexMat.Width * indexMat.Height)}"); + return true; + } + + return false; + } + + + /// + /// 使用出战标识识别出战 + /// + /// + /// + /// + public static int FindActiveIndexRectByArrow(ImageRegion imageRegion, Rect[] rectArray) + { + if (rectArray.Length == 1) + { + return 1; + } + + var curr = imageRegion.Find(ElementAssets.Instance.CurrentAvatarThreshold); // 当前出战角色标识 + if (curr.IsEmpty()) + { + return -1; + } + + for (int i = 0; i < rectArray.Length; i++) + { + if (IsIntersecting(curr.Y, curr.Height, rectArray[i].Y, rectArray[i].Height)) + { + return i + 1; + } + } + + return -1; + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/AutoFight/Script/CombatCommand.cs b/BetterGenshinImpact/GameTask/AutoFight/Script/CombatCommand.cs index be9df420..34b1e75c 100644 --- a/BetterGenshinImpact/GameTask/AutoFight/Script/CombatCommand.cs +++ b/BetterGenshinImpact/GameTask/AutoFight/Script/CombatCommand.cs @@ -64,7 +64,7 @@ public class CombatCommand } } - public void Execute(CombatScenes combatScenes) + public void Execute(CombatScenes combatScenes, CombatCommand? lastCommand = null) { Avatar? avatar; if (Name == CombatScriptParser.CurrentAvatarName) @@ -79,18 +79,27 @@ public class CombatCommand { return; } - // 非宏类脚本,等待切换角色成功 - if (Method != Method.Wait - && Method != Method.MouseDown - && Method != Method.MouseUp - && Method != Method.Click - && Method != Method.MoveBy - && Method != Method.KeyDown - && Method != Method.KeyUp - && Method != Method.KeyPress) + + if (lastCommand != null && lastCommand.Name != Name) { + // 上一个命令和当前命令不是同一个角色,直接切换角色 avatar.Switch(); } + else + { + // 非宏类脚本,等待切换角色成功 + if (Method != Method.Wait + && Method != Method.MouseDown + && Method != Method.MouseUp + && Method != Method.Click + && Method != Method.MoveBy + && Method != Method.KeyDown + && Method != Method.KeyUp + && Method != Method.KeyPress) + { + avatar.Switch(); + } + } } Execute(avatar); } diff --git a/BetterGenshinImpact/GameTask/AutoFishing/AutoFishingTask.cs b/BetterGenshinImpact/GameTask/AutoFishing/AutoFishingTask.cs index c313867b..2217a8cb 100644 --- a/BetterGenshinImpact/GameTask/AutoFishing/AutoFishingTask.cs +++ b/BetterGenshinImpact/GameTask/AutoFishing/AutoFishingTask.cs @@ -390,7 +390,7 @@ namespace BetterGenshinImpact.GameTask.AutoFishing clickWhiteConfirmButtonWaitEndTime < timeProvider.GetLocalNow()) && Bv.ClickWhiteConfirmButton(imageRegion)) { - Mat subMat = imageRegion.SrcMat.SubMat(new Rect((int)(0.824 * imageRegion.Width), (int)(0.669 * imageRegion.Height), (int)(0.065 * imageRegion.Width), (int)(0.065 * imageRegion.Width))); + using Mat subMat = imageRegion.SrcMat.SubMat(new Rect((int)(0.824 * imageRegion.Width), (int)(0.669 * imageRegion.Height), (int)(0.065 * imageRegion.Width), (int)(0.065 * imageRegion.Width))); using Mat resized = subMat.Resize(new Size(125, 125)); (string predName, _) = GridIconsAccuracyTestTask.Infer(resized, this.session, this.prototypes); if (predName.TryGetEnumValueFromDescription(out this.blackboard.selectedBait)) diff --git a/BetterGenshinImpact/GameTask/AutoFishing/Behaviours.cs b/BetterGenshinImpact/GameTask/AutoFishing/Behaviours.cs index 3b4696a7..dff8f0c8 100644 --- a/BetterGenshinImpact/GameTask/AutoFishing/Behaviours.cs +++ b/BetterGenshinImpact/GameTask/AutoFishing/Behaviours.cs @@ -18,13 +18,13 @@ 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; using System.Linq; using static Vanara.PInvoke.User32; using Color = System.Drawing.Color; -using Pen = System.Drawing.Pen; namespace BetterGenshinImpact.GameTask.AutoFishing { @@ -151,7 +151,7 @@ namespace BetterGenshinImpact.GameTask.AutoFishing // 寻找鱼饵 var boxAndBaits = FindBait(imageRegion); - ; + ; foreach ((Rect box, string? predName) in boxAndBaits) { if (predName == blackboard.selectedBait.GetDescription()) @@ -232,9 +232,15 @@ namespace BetterGenshinImpact.GameTask.AutoFishing using ImageRegion resRa = singleRowGrid.DeriveCrop(box); using Mat img125 = resRa.SrcMat.GetGridIcon(); (string? predName, _) = GridIconsAccuracyTestTask.Infer(img125, this.session, this.prototypes); + if (predName != null && !availableBaitNames.Contains(predName)) + { + predName = null; + } yield return (new Rect(singleRowGrid.X + box.X, singleRowGrid.Y + box.Y, box.Width, box.Height), predName); } } + + private static readonly FrozenSet availableBaitNames = Enum.GetValues(typeof(BaitType)).Cast().Select(bt => bt.GetDescription()).ToFrozenSet(); } [Obsolete] @@ -450,7 +456,7 @@ namespace BetterGenshinImpact.GameTask.AutoFishing else { noTargetFishTimes = 0; - imageRegion.DrawRect(fishpondTargetRect, "Target", new Pen(Color.White)); + imageRegion.DrawRect(fishpondTargetRect, "Target", System.Drawing.Pens.White); imageRegion.Derive(currentFish.Rect).DrawSelf("Fish"); // drawContent.PutRect("Target", fishpond.TargetRect.ToRectDrawable()); @@ -855,7 +861,8 @@ namespace BetterGenshinImpact.GameTask.AutoFishing (topMat.Width / 2 - _cur.X) * 2 + hExtra * 2, _cur.Height + vExtra * 2); // VisionContext.Instance().DrawContent.PutRect("FishBox", _fishBoxRect.ToRectDrawable(new Pen(Color.LightPink, 2))); using var boxRa = imageRegion.Derive(blackboard.fishBoxRect); - boxRa.DrawSelf("FishBox", new Pen(Color.LightPink, 2)); + using var pen = new System.Drawing.Pen(Color.LightPink, 2); + boxRa.DrawSelf("FishBox", pen); logger.LogInformation(" 识别到钓鱼框"); return BehaviourStatus.Succeeded; } @@ -1018,21 +1025,20 @@ namespace BetterGenshinImpact.GameTask.AutoFishing return BehaviourStatus.Running; } - private readonly Pen _pen = new(Color.Red, 1); private void PutRects(ImageRegion imageRegion, Rect left, Rect cur, Rect right) { //var list = new List //{ - // left.ToWindowsRectangleOffset(_fishBoxRect.X, _fishBoxRect.Y).ToRectDrawable(_pen), - // cur.ToWindowsRectangleOffset(_fishBoxRect.X, _fishBoxRect.Y).ToRectDrawable(_pen), - // right.ToWindowsRectangleOffset(_fishBoxRect.X, _fishBoxRect.Y).ToRectDrawable(_pen) + // left.ToWindowsRectangleOffset(_fishBoxRect.X, _fishBoxRect.Y).ToRectDrawable(System.Drawing.Pens.Red), + // cur.ToWindowsRectangleOffset(_fishBoxRect.X, _fishBoxRect.Y).ToRectDrawable(System.Drawing.Pens.Red), + // right.ToWindowsRectangleOffset(_fishBoxRect.X, _fishBoxRect.Y).ToRectDrawable(System.Drawing.Pens.Red) //}; using var fishBoxRa = imageRegion.Derive(blackboard.fishBoxRect); var list = new List { - fishBoxRa.ToRectDrawable(left, "left", _pen), - fishBoxRa.ToRectDrawable(cur, "cur", _pen), - fishBoxRa.ToRectDrawable(right, "right", _pen), + fishBoxRa.ToRectDrawable(left, "left", System.Drawing.Pens.Red), + fishBoxRa.ToRectDrawable(cur, "cur", System.Drawing.Pens.Red), + fishBoxRa.ToRectDrawable(right, "right", System.Drawing.Pens.Red), }.Where(r => r.Rect.Height != 0).ToList(); drawContent.PutOrRemoveRectList("FishingBarAll", list); } diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/tcg_character_card.json b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/tcg_character_card.json index 7aa58332..a1cff844 100644 --- a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/tcg_character_card.json +++ b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/tcg_character_card.json @@ -3999,6 +3999,83 @@ } ] }, + { + "id": 1415, + "nameEn": "varesa", + "type": "character", + "name": "瓦雷莎", + "hp": 10, + "energy": 3, + "element": "雷元素", + "weapon": "法器", + "skills": [ + { + "nameEn": "by_the_horns", + "name": "角力搏摔", + "skillTag": [ + "普通攻击" + ], + "cost": [ + { + "id": 1104, + "nameEn": "electro", + "type": "雷元素", + "count": 1 + }, + { + "id": 1109, + "nameEn": "unaligned_element", + "type": "无色元素", + "count": 2 + } + ] + }, + { + "nameEn": "riding_the_nightrainbow", + "name": "夜虹逐跃", + "skillTag": [ + "元素战技" + ], + "cost": [ + { + "id": 1104, + "nameEn": "electro", + "type": "雷元素", + "count": 3 + } + ] + }, + { + "nameEn": "guardian_vent", + "name": "闪烈降临!", + "skillTag": [ + "元素爆发" + ], + "cost": [ + { + "id": 1104, + "nameEn": "electro", + "type": "雷元素", + "count": 3 + }, + { + "id": 1110, + "nameEn": "energy", + "type": "充能", + "count": 3 + } + ] + }, + { + "nameEn": "tagteam_triple_jump", + "name": "连势,三重腾跃!", + "skillTag": [ + "被动技能" + ], + "cost": [] + } + ] + }, { "id": 1501, "nameEn": "sucrose", @@ -4981,6 +5058,75 @@ } ] }, + { + "id": 1515, + "nameEn": "ifa", + "type": "character", + "name": "伊法", + "hp": 10, + "energy": 2, + "element": "风元素", + "weapon": "法器", + "skills": [ + { + "nameEn": "rite_of_dispelling_winds", + "name": "祛风妙仪", + "skillTag": [ + "普通攻击" + ], + "cost": [ + { + "id": 1105, + "nameEn": "anemo", + "type": "风元素", + "count": 1 + }, + { + "id": 1109, + "nameEn": "unaligned_element", + "type": "无色元素", + "count": 2 + } + ] + }, + { + "nameEn": "airborne_disease_prevention", + "name": "空天疾护", + "skillTag": [ + "元素战技" + ], + "cost": [ + { + "id": 1105, + "nameEn": "anemo", + "type": "风元素", + "count": 2 + } + ] + }, + { + "nameEn": "compound_sedation_field", + "name": "复合镇静域", + "skillTag": [ + "元素爆发" + ], + "cost": [ + { + "id": 1105, + "nameEn": "anemo", + "type": "风元素", + "count": 3 + }, + { + "id": 1110, + "nameEn": "energy", + "type": "充能", + "count": 2 + } + ] + } + ] + }, { "id": 1601, "nameEn": "ningguang", @@ -7160,6 +7306,83 @@ } ] }, + { + "id": 2206, + "nameEn": "hydro_tulpa", + "type": "character", + "name": "水形幻人", + "hp": 12, + "energy": 3, + "element": "水元素", + "weapon": "其他武器", + "skills": [ + { + "nameEn": "savage_swell", + "name": "涌浪", + "skillTag": [ + "普通攻击" + ], + "cost": [ + { + "id": 1102, + "nameEn": "hydro", + "type": "水元素", + "count": 1 + }, + { + "id": 1109, + "nameEn": "unaligned_element", + "type": "无色元素", + "count": 2 + } + ] + }, + { + "nameEn": "storm_surge", + "name": "汛波", + "skillTag": [ + "元素战技" + ], + "cost": [ + { + "id": 1102, + "nameEn": "hydro", + "type": "水元素", + "count": 3 + } + ] + }, + { + "nameEn": "thundering_tide", + "name": "洪啸", + "skillTag": [ + "元素爆发" + ], + "cost": [ + { + "id": 1102, + "nameEn": "hydro", + "type": "水元素", + "count": 3 + }, + { + "id": 1110, + "nameEn": "energy", + "type": "充能", + "count": 3 + } + ] + }, + { + "nameEn": "branching_flow", + "name": "分流", + "skillTag": [ + "被动技能" + ], + "cost": [] + } + ] + }, { "id": 2301, "nameEn": "fatui_pyro_agent", diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/ScriptParser.cs b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/ScriptParser.cs index fb5cdb37..0ba12071 100644 --- a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/ScriptParser.cs +++ b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/ScriptParser.cs @@ -2,6 +2,7 @@ using BetterGenshinImpact.GameTask.AutoGeniusInvokation.Config; using BetterGenshinImpact.GameTask.AutoGeniusInvokation.Model; using BetterGenshinImpact.Helpers; +using BetterGenshinImpact.View.Windows; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; @@ -95,7 +96,7 @@ public class ScriptParser catch (System.Exception ex) { MyLogger.LogError($"解析脚本错误,行号:{i + 1},错误信息:{ex}"); - MessageBox.Error($"解析脚本错误,行号:{i + 1},错误信息:{ex}", "策略解析失败"); + ThemedMessageBox.Error($"解析脚本错误,行号:{i + 1},错误信息:{ex}", "策略解析失败"); return default!; } diff --git a/BetterGenshinImpact/GameTask/AutoPathing/CameraRotateTask.cs b/BetterGenshinImpact/GameTask/AutoPathing/CameraRotateTask.cs index 5b7f5e73..c2e865cb 100644 --- a/BetterGenshinImpact/GameTask/AutoPathing/CameraRotateTask.cs +++ b/BetterGenshinImpact/GameTask/AutoPathing/CameraRotateTask.cs @@ -56,14 +56,16 @@ public class CameraRotateTask(CancellationToken ct) /// 最大误差 /// 最大尝试次数(超时时间) /// - public async Task WaitUntilRotatedTo(int targetOrientation, int maxDiff, int maxTryTimes = 50) + public async Task WaitUntilRotatedTo(int targetOrientation, int maxDiff, int maxTryTimes = 50) { + bool isSuccessful = false; int count = 0; while (!ct.IsCancellationRequested) { var screen = CaptureToRectArea(); if (Math.Abs(RotateToApproach(targetOrientation, screen)) < maxDiff) { + isSuccessful = true; break; } @@ -76,5 +78,6 @@ public class CameraRotateTask(CancellationToken ct) await Delay(50, ct); count++; } + return isSuccessful; } } diff --git a/BetterGenshinImpact/GameTask/AutoPathing/Handler/ActionFactory.cs b/BetterGenshinImpact/GameTask/AutoPathing/Handler/ActionFactory.cs index a8586249..7cc1e749 100644 --- a/BetterGenshinImpact/GameTask/AutoPathing/Handler/ActionFactory.cs +++ b/BetterGenshinImpact/GameTask/AutoPathing/Handler/ActionFactory.cs @@ -29,6 +29,7 @@ public class ActionFactory "exit_and_relogin" => new ExitAndReloginHandler(), "set_time" => new SetTimeHandler(), "use_gadget" => new UseGadgetHandler(), + "pick_up_collect" => new PickUpCollectHandler(), _ => throw new ArgumentException("未知的后置 action 类型") }; }); diff --git a/BetterGenshinImpact/GameTask/AutoPathing/Handler/CombatScriptHandler.cs b/BetterGenshinImpact/GameTask/AutoPathing/Handler/CombatScriptHandler.cs index d781b339..1fe0b2b8 100644 --- a/BetterGenshinImpact/GameTask/AutoPathing/Handler/CombatScriptHandler.cs +++ b/BetterGenshinImpact/GameTask/AutoPathing/Handler/CombatScriptHandler.cs @@ -43,10 +43,12 @@ public class CombatScriptHandler : IActionHandler try { // 通用化战斗策略 - foreach (var command in combatScript.CombatCommands) + for (var i = 0; i < combatScript.CombatCommands.Count; i++) { + var command = combatScript.CombatCommands[i]; + var lastCommand = i == 0 ? command : combatScript.CombatCommands[i - 1]; ct.ThrowIfCancellationRequested(); - command.Execute(combatScenes); + command.Execute(combatScenes, lastCommand); } } catch (RetryException e) diff --git a/BetterGenshinImpact/GameTask/AutoPathing/Handler/PickUpCollectHandler.cs b/BetterGenshinImpact/GameTask/AutoPathing/Handler/PickUpCollectHandler.cs new file mode 100644 index 00000000..1c8c9055 --- /dev/null +++ b/BetterGenshinImpact/GameTask/AutoPathing/Handler/PickUpCollectHandler.cs @@ -0,0 +1,238 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using BetterGenshinImpact.GameTask.AutoPathing.Model; +using Microsoft.Extensions.Logging; +using static BetterGenshinImpact.GameTask.Common.TaskControl; +using BetterGenshinImpact.GameTask.AutoFight.Model; +using BetterGenshinImpact.GameTask.AutoFight.Script; +using System.Linq; +using System.Collections.Generic; +using BetterGenshinImpact.GameTask.AutoFight.Config; + +namespace BetterGenshinImpact.GameTask.AutoPathing.Handler; + + /// +/// 使用万叶或琴团通过战技吸取拾取物品,优先万叶,如果没有万叶则使用琴团 +/// +public class PickUpCollectHandler : IActionHandler +{ + /// + /// 新增命令时,以角色名称开头(必填),“-”后定义动作(必填,用于预解析,不能单写角色名称),空格后定义参数(必填) + /// 1、"action": "pick_up_collect","action_params":为空或不填,在队伍中寻找CharacterNames中第一个找到的角色,找到就会执行PickUpActions第一个找到的相关角色命令。 + /// 2、"action": "pick_up_collect","action_params":"琴",只填角色名称(或别名),会执行PickUpActions第一个找到的相关角色命令。 + /// 3、"action": "pick_up_collect","action_params":"琴-短E",填了具体角色和动作,直接找PickUpActions找到该命令执行。 + /// + public static readonly string[] PickUpActions = + [ + "枫原万叶-长E keydown(E),wait(0.7),keyup(E),attack(0.2),wait(0.5)", + "枫原万叶-短E e,attack(0.15)", + "琴-短E wait(0.1),keydown(E),wait(0.4),moveby(1000,0),wait(0.2),moveby(1000,0),wait(0.2),moveby(1000,0),wait(0.2),moveby(1000,-3500),wait(1.8),keyup(E),wait(0.3),click(middle)", + "琴-长E wait(0.1),click(middle),keydown(E),click(middle),wait(0.4),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1)," + + "moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1)," + + "moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1)," + + "moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1)," + + "moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1)," + + "moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1)," + + "moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1)," + + "moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),wait(0.1)," + + "moveby(500,0),wait(0.1),moveby(500,0),wait(0.1),moveby(500,0),moveby(1000,3500),wait(1.8),keyup(E),wait(0.3),click(middle),wait(0.3)", + ]; + + // 预解析所有角色名 + private static readonly HashSet CharacterNames = new HashSet( + PickUpActions + .Select(action => action.Split(' ')[0]) + .Select(GetBaseCharacterName) + .Distinct() + ); + + public async Task RunAsync(CancellationToken ct, WaypointForTrack? waypointForTrack = null, object? config = null) + { + Logger.LogInformation("简易策略:执行 {Nhd} 动作","聚集材料"); + + var combatScenes = await RunnerContext.Instance.GetCombatScenes(ct); + if (combatScenes == null) + { + Logger.LogError("队伍识别未初始化成功!"); + return; + } + + Avatar? picker = null; + var commandsList = new List(); + + if (waypointForTrack != null) + { + if (!string.IsNullOrEmpty(waypointForTrack.ActionParams)) + { + var commands = waypointForTrack.ActionParams.Split(','); + + foreach (var command in commands) + { + + try + { + var alias = DefaultAutoFightConfig.AvatarAliasToStandardName(command); + commandsList.Add(!string.IsNullOrEmpty(alias) ? alias : command); + } + catch (Exception e) + { + commandsList.Add(command); + Console.WriteLine(e); + } + } + } + else + { + // 1、ActionParams没填参数,尝试选择,如果找到,后续会执行第一个找到该角色的相关命令 + foreach (var characterName in CharacterNames) + { + var pickerNull = combatScenes.SelectAvatar(characterName); + if (pickerNull is null) + { + continue; + } + commandsList.Add(characterName); + break; + } + } + } + + foreach (var commands in commandsList) + { + if (CharacterNames.Contains(commands)) + { + picker = combatScenes.SelectAvatar(commands); + } + else + { + var characterName = GetCharacterName(commands); + picker = combatScenes.SelectAvatar(characterName); + } + + if (picker is not null) + { + picker.TrySwitch(); + await picker.WaitSkillCd(ct); + } + else + { + continue; + } + + PickUpMaterial(combatScenes,commands); // 开始执行动作 + } + } + + /// + /// 执行聚集材料动作 + /// + /// + /// + private void PickUpMaterial(CombatScenes combatScenes, string? pickerName = null) + { + try + { + var foundAvatar = false; + string[] actionsToUse; + var characterName = string.Empty; + + if (pickerName != null) + { + actionsToUse = PickUpActions.Where(action => + action.StartsWith(pickerName + " ", StringComparison.OrdinalIgnoreCase)).ToArray(); + + if (actionsToUse.Length == 0) + { + if (CharacterNames.Contains(pickerName)) //2.只填了角色名,则用基础角色名筛选,执行pickerName相关的第一个命令 + { + var actions = PickUpActions.FirstOrDefault(action => action.StartsWith(pickerName, StringComparison.OrdinalIgnoreCase)); + actionsToUse = actions == null ? new string[0] : new string[] {actions}; + + // 替换第一个空格前的字符为 pickerName + if (actionsToUse.Length > 0 && actionsToUse[0].Contains(' ')) + { + string action = actionsToUse[0]; + int firstSpaceIndex = action.IndexOf(' '); + actionsToUse[0] = pickerName + action.Substring(firstSpaceIndex); + } + } + else + { + Logger.LogError($"未找到角色 {pickerName} 对应的动作"); + return; + } + } + else + { + // 提取角色名称 + characterName = GetCharacterName(pickerName); + + // 3.填了具体命令,则用具体命令筛选,并将命令中的角色替换为角色名称 + actionsToUse = actionsToUse + .Select(action => action.Replace(pickerName + " ", characterName + " ", StringComparison.OrdinalIgnoreCase)) + .ToArray(); + } + } + else + { + Logger.LogError("未找到ActionParams"); + return; + } + + foreach (var pickUpActionStr in actionsToUse) + { + var pickUpAction = CombatScriptParser.ParseContext(pickUpActionStr); + foreach (var command in pickUpAction.CombatCommands) + { + var avatar = combatScenes.SelectAvatar(command.Name); + if (avatar != null) + { + command.Execute(combatScenes); + foundAvatar = true; + } + } + if (foundAvatar) + { + var selectedAvatar = combatScenes.SelectAvatar(characterName); + if (selectedAvatar is not null) + { + Sleep(200);//等待CD显示 + selectedAvatar.AfterUseSkill(); + } + break; + } + } + } + catch (Exception ex) + { + // 处理异常 + Console.WriteLine($"PickUpCollectHandler 异常: {ex.Message}"); + } + } + + // 直接匹配预解析的角色名 + private static string GetCharacterName(string pickerName) + { + foreach (var name in CharacterNames) + { + if (pickerName.StartsWith(name)) + return name; + } + + return pickerName; + } + + /// + /// 从完整动作名提取基础角色名 + /// + private static string GetBaseCharacterName(string fullActionName) + { + // 找到第一个"-"号的位置 + var dashIndex = fullActionName.IndexOf('-'); + + // 如果存在"-"号,则返回"-"号前的部分 + return dashIndex > 0 ? fullActionName.Substring(0, dashIndex) : string.Empty; + } + +} diff --git a/BetterGenshinImpact/GameTask/AutoPathing/Model/Enum/ActionEnum.cs b/BetterGenshinImpact/GameTask/AutoPathing/Model/Enum/ActionEnum.cs index 4b0aa9b4..ec263372 100644 --- a/BetterGenshinImpact/GameTask/AutoPathing/Model/Enum/ActionEnum.cs +++ b/BetterGenshinImpact/GameTask/AutoPathing/Model/Enum/ActionEnum.cs @@ -25,7 +25,7 @@ public class ActionEnum(string code, string msg, ActionUseWaypointTypeEnum useWa public static readonly ActionEnum ExitAndRelogin = new("exit_and_relogin", "退出重新登录", ActionUseWaypointTypeEnum.Custom); public static readonly ActionEnum SetTime = new("set_time", "设置时间", ActionUseWaypointTypeEnum.Custom); public static readonly ActionEnum UseGadget = new("use_gadget", "使用小道具", ActionUseWaypointTypeEnum.Custom); - + public static readonly ActionEnum PickUpCollect = new("pick_up_collect", "聚集材料", ActionUseWaypointTypeEnum.Custom); // 还有要加入的其他动作 // 滚轮F diff --git a/BetterGenshinImpact/GameTask/AutoPathing/Model/PathingTaskInfo.cs b/BetterGenshinImpact/GameTask/AutoPathing/Model/PathingTaskInfo.cs index 20f82597..d9f705a3 100644 --- a/BetterGenshinImpact/GameTask/AutoPathing/Model/PathingTaskInfo.cs +++ b/BetterGenshinImpact/GameTask/AutoPathing/Model/PathingTaskInfo.cs @@ -56,7 +56,7 @@ public class PathingTaskInfo /// SIFT 老的匹配方式 /// TemplateMatch 支持分层地图 /// - public string MapMatchMethod { get; set; } = "SIFT"; + public string MapMatchMethod { get; set; } = TaskContext.Instance().Config.PathingConditionConfig.MapMatchingMethod; public List Items { get; set; } = []; diff --git a/BetterGenshinImpact/GameTask/AutoPathing/Model/WaypointForTrack.cs b/BetterGenshinImpact/GameTask/AutoPathing/Model/WaypointForTrack.cs index 524878a1..a441d965 100644 --- a/BetterGenshinImpact/GameTask/AutoPathing/Model/WaypointForTrack.cs +++ b/BetterGenshinImpact/GameTask/AutoPathing/Model/WaypointForTrack.cs @@ -20,6 +20,9 @@ public class WaypointForTrack : Waypoint public double MatY { get; set; } public string MapName { get; set; } + + public string MapMatchMethod { get; set; } + //异常识别 public Misidentification Misidentification { get; set; } = new(); @@ -38,7 +41,7 @@ public class WaypointForTrack : Waypoint /// public string? LogInfo { get; set; } - public WaypointForTrack(Waypoint waypoint, string mapName) + public WaypointForTrack(Waypoint waypoint, string mapName, string? mapMatchMethod) { Type = waypoint.Type; MoveMode = waypoint.MoveMode; @@ -48,7 +51,9 @@ public class WaypointForTrack : Waypoint GameY = waypoint.Y; MapName = mapName; // 坐标系转换 - (MatX, MatY) = MapManager.GetMap(mapName).ConvertGenshinMapCoordinatesToImageCoordinates((float)waypoint.X, (float)waypoint.Y); + mapMatchMethod ??= TaskContext.Instance().Config.PathingConditionConfig.MapMatchingMethod; + MapMatchMethod = mapMatchMethod; + (MatX, MatY) = MapManager.GetMap(mapName, MapMatchMethod).ConvertGenshinMapCoordinatesToImageCoordinates((float)waypoint.X, (float)waypoint.Y); X = MatX; Y = MatY; if (waypoint.Action == ActionEnum.CombatScript.Code) diff --git a/BetterGenshinImpact/GameTask/AutoPathing/Navigation.cs b/BetterGenshinImpact/GameTask/AutoPathing/Navigation.cs index 16fc4a2e..166f4cdd 100644 --- a/BetterGenshinImpact/GameTask/AutoPathing/Navigation.cs +++ b/BetterGenshinImpact/GameTask/AutoPathing/Navigation.cs @@ -16,11 +16,11 @@ public class Navigation private static readonly NavigationInstance _instance = new(); - public static void WarmUp() + public static void WarmUp(string mapMatchMethod) { if (!_isWarmUp) { - MapManager.GetMap(MapTypes.Teyvat); + MapManager.GetMap(MapTypes.Teyvat, mapMatchMethod); } _isWarmUp = true; @@ -37,9 +37,9 @@ public class Navigation _instance.SetPrevPosition(x,y); } - public static Point2f GetPosition(ImageRegion imageRegion, string mapName) + public static Point2f GetPosition(ImageRegion imageRegion, string mapName, string mapMatchMethod) { - return _instance.GetPosition(imageRegion, mapName); + return _instance.GetPosition(imageRegion, mapName, mapMatchMethod); } /// @@ -47,10 +47,11 @@ public class Navigation /// /// 图像区域 /// + /// /// 当前位置坐标 - public static Point2f GetPositionStable(ImageRegion imageRegion, string mapName) + public static Point2f GetPositionStable(ImageRegion imageRegion, string mapName, string mapMatchMethod) { - return _instance.GetPositionStable(imageRegion, mapName); + return _instance.GetPositionStable(imageRegion, mapName, mapMatchMethod); } public static int GetTargetOrientation(Waypoint waypoint, Point2f position) diff --git a/BetterGenshinImpact/GameTask/AutoPathing/NavigationInstance.cs b/BetterGenshinImpact/GameTask/AutoPathing/NavigationInstance.cs index d447c746..d7d5d220 100644 --- a/BetterGenshinImpact/GameTask/AutoPathing/NavigationInstance.cs +++ b/BetterGenshinImpact/GameTask/AutoPathing/NavigationInstance.cs @@ -24,11 +24,11 @@ public class NavigationInstance (_prevX, _prevY) = (x, y); } - public Point2f GetPosition(ImageRegion imageRegion, string mapName) + public Point2f GetPosition(ImageRegion imageRegion, string mapName, string mapMatchMethod) { var colorMat = new Mat(imageRegion.SrcMat, MapAssets.Instance.MimiMapRect); var captureTime = DateTime.UtcNow; - var p = MapManager.GetMap(mapName).GetMiniMapPosition(colorMat, _prevX, _prevY); + var p = MapManager.GetMap(mapName, mapMatchMethod).GetMiniMapPosition(colorMat, _prevX, _prevY); if (p != default && captureTime > _captureTime) { (_prevX, _prevY) = (p.X, p.Y); @@ -43,15 +43,16 @@ public class NavigationInstance /// 稳定获取当前位置坐标,优先使用全地图匹配,适用于不需要高效率但需要高稳定性的场景 /// /// 图像区域 - /// + /// 地图名字 + /// 地图匹配方式 /// 当前位置坐标 - public Point2f GetPositionStable(ImageRegion imageRegion, string mapName) + public Point2f GetPositionStable(ImageRegion imageRegion, string mapName, string mapMatchMethod) { var colorMat = new Mat(imageRegion.SrcMat, MapAssets.Instance.MimiMapRect); var captureTime = DateTime.UtcNow; // 先尝试使用局部匹配 - var sceneMap = MapManager.GetMap(mapName); + var sceneMap = MapManager.GetMap(mapName, mapMatchMethod); //提高局部匹配的阈值,以解决在沙漠录制点位时,移动过远不会触发全局匹配的情况 var p = (sceneMap as SceneBaseMapByTemplateMatch)?.GetMiniMapPosition(colorMat, _prevX, _prevY, 0) ?? sceneMap.GetMiniMapPosition(colorMat, _prevX, _prevY); @@ -60,7 +61,7 @@ public class NavigationInstance if (p == default || (_prevX > 0 && _prevY >0 && p.DistanceTo(new Point2f(_prevX,_prevY)) > 150)) { Reset(); - p = MapManager.GetMap(mapName).GetMiniMapPosition(colorMat, _prevX, _prevY); + p = MapManager.GetMap(mapName, mapMatchMethod).GetMiniMapPosition(colorMat, _prevX, _prevY); } if (p != default && captureTime > _captureTime) { @@ -73,13 +74,14 @@ public class NavigationInstance return p; } - public Point2f GetPositionStableByCache(ImageRegion imageRegion, string mapName, int cacheTimeMs = 900) + public Point2f GetPositionStableByCache(ImageRegion imageRegion, string mapName, string mapMatchingMethod, int cacheTimeMs = 900) { var captureTime = DateTime.UtcNow; if (captureTime - _captureTime < TimeSpan.FromMilliseconds(cacheTimeMs) && _prevX > 0 && _prevY > 0) { return new Point2f(_prevX, _prevY); } - return GetPositionStable(imageRegion, mapName); + + return GetPositionStable(imageRegion, mapName, mapMatchingMethod); } } \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/AutoPathing/PathExecutor.cs b/BetterGenshinImpact/GameTask/AutoPathing/PathExecutor.cs index f8615d88..01fbca79 100644 --- a/BetterGenshinImpact/GameTask/AutoPathing/PathExecutor.cs +++ b/BetterGenshinImpact/GameTask/AutoPathing/PathExecutor.cs @@ -37,6 +37,7 @@ using BetterGenshinImpact.GameTask.AutoPathing; using BetterGenshinImpact.GameTask.Common.Element.Assets; using BetterGenshinImpact.GameTask.Common.Exceptions; using BetterGenshinImpact.GameTask.Common.Map.Maps; +using BetterGenshinImpact.GameTask.AutoFight; namespace BetterGenshinImpact.GameTask.AutoPathing; @@ -160,7 +161,7 @@ public class PathExecutor var waypointsList = ConvertWaypointsForTrack(task.Positions, task); await Delay(100, ct); - Navigation.WarmUp(); // 提前加载地图特征点 + Navigation.WarmUp(task.Info.MapMatchMethod); // 提前加载地图特征点 foreach (var waypoints in waypointsList) // 按传送点分割的路径 { @@ -213,6 +214,9 @@ public class PathExecutor if ((!string.IsNullOrEmpty(waypoint.Action) && !_skipOtherOperations) || waypoint.Action == ActionEnum.CombatScript.Code) { + //战斗前的节点记录,用于游泳检测回到战斗节点 + AutoFightTask.FightWaypoint = waypoint.Action == ActionEnum.Fight.Code ? waypoint : null; + // 执行 action await AfterMoveToTarget(waypoint); } @@ -505,7 +509,7 @@ public class PathExecutor // 把 X Y 转换为 MatX MatY var allList = positions.Select(waypoint => { - WaypointForTrack wft=new WaypointForTrack(waypoint, task.Info.MapName); + WaypointForTrack wft = new WaypointForTrack(waypoint, task.Info.MapName, task.Info.MapMatchMethod); wft.Misidentification=waypoint.PointExtParams.Misidentification; wft.MonsterTag = waypoint.PointExtParams.MonsterTag; wft.EnableMonsterLootSplit = waypoint.PointExtParams.EnableMonsterLootSplit; @@ -633,9 +637,9 @@ public class PathExecutor { tpTask = new TpTask(ct); } - + // 最小5分钟间隔 - if ((DateTime.UtcNow - _lastGetExpeditionRewardsTime).TotalMinutes < 5) + if ( _combatScenes?.CurrentMultiGameStatus?.IsInMultiGame == true || (DateTime.UtcNow - _lastGetExpeditionRewardsTime).TotalMinutes < 5) { return false; } @@ -648,7 +652,6 @@ public class PathExecutor if (!RunnerContext.Instance.isAutoFetchDispatch && adventurersGuildCountry != "无") { var ra1 = CaptureToRectArea(); - var textRect = new Rect(60, 20, 160, 260); var textMat = new Mat(ra1.SrcMat, textRect); string text = OcrFactory.Paddle.Ocr(textMat); @@ -685,7 +688,7 @@ public class PathExecutor TpTask tpTask = new TpTask(ct); await TryGetExpeditionRewardsDispatch(tpTask); var (tpX, tpY) = await tpTask.Tp(waypoint.GameX, waypoint.GameY, waypoint.MapName, forceTp); - var (tprX, tprY) = MapManager.GetMap(waypoint.MapName) + var (tprX, tprY) = MapManager.GetMap(waypoint.MapName, waypoint.MapMatchMethod) .ConvertGenshinMapCoordinatesToImageCoordinates((float)tpX, (float)tpY); Navigation.SetPrevPosition(tprX, tprY); // 通过上一个位置直接进行局部特征匹配 await Delay(500, ct); // 多等一会 @@ -697,7 +700,7 @@ public class PathExecutor var position = await GetPosition(screen, waypoint); var targetOrientation = Navigation.GetTargetOrientation(waypoint, position); Logger.LogDebug("朝向点,位置({x2},{y2})", $"{waypoint.GameX:F1}", $"{waypoint.GameY:F1}"); - await _rotateTask.WaitUntilRotatedTo(targetOrientation, 2); + await WaitUntilRotatedTo(targetOrientation, 2); await Delay(500, ct); } @@ -712,7 +715,7 @@ public class PathExecutor var (position, additionalTimeInMs) = await GetPositionAndTime(screen, waypoint); var targetOrientation = Navigation.GetTargetOrientation(waypoint, position); Logger.LogDebug("粗略接近途经点,位置({x2},{y2})", $"{waypoint.GameX:F1}", $"{waypoint.GameY:F1}"); - await _rotateTask.WaitUntilRotatedTo(targetOrientation, 5); + await WaitUntilRotatedTo(targetOrientation, 5); moveToStartTime = DateTime.UtcNow; var lastPositionRecord = DateTime.UtcNow; var fastMode = false; @@ -842,7 +845,7 @@ public class PathExecutor if (consecutiveRotationCountBeyondAngle > 10) { // 直接站定好转向 - await _rotateTask.WaitUntilRotatedTo(targetOrientation, 2); + await WaitUntilRotatedTo(targetOrientation, 2); } } @@ -1016,7 +1019,7 @@ public class PathExecutor } targetOrientation = Navigation.GetTargetOrientation(waypoint, position); - await _rotateTask.WaitUntilRotatedTo(targetOrientation, 2); + await WaitUntilRotatedTo(targetOrientation, 2); // 小碎步接近 Simulation.SendInput.SimulateAction(GIActions.MoveForward, KeyType.KeyDown); Thread.Sleep(60); @@ -1048,7 +1051,7 @@ public class PathExecutor var screen = CaptureToRectArea(); var position = await GetPosition(screen, waypoint); var targetOrientation = Navigation.GetTargetOrientation(waypoint, position); - await _rotateTask.WaitUntilRotatedTo(targetOrientation, 10); + await WaitUntilRotatedTo(targetOrientation, 10); var handler = ActionFactory.GetBeforeHandler(waypoint.Action); await handler.RunAsync(ct, waypoint); } @@ -1072,7 +1075,8 @@ public class PathExecutor || waypoint.Action == ActionEnum.Fishing.Code || waypoint.Action == ActionEnum.ExitAndRelogin.Code || waypoint.Action == ActionEnum.SetTime.Code - || waypoint.Action == ActionEnum.UseGadget.Code) + || waypoint.Action == ActionEnum.UseGadget.Code + || waypoint.Action == ActionEnum.PickUpCollect.Code) { var handler = ActionFactory.GetAfterHandler(waypoint.Action); //,PartyConfig @@ -1178,7 +1182,7 @@ public class PathExecutor private async Task<(Point2f point,int additionalTimeInMs)> GetPositionAndTime(ImageRegion imageRegion, WaypointForTrack waypoint) { - var position = Navigation.GetPosition(imageRegion, waypoint.MapName); + var position = Navigation.GetPosition(imageRegion, waypoint.MapName, waypoint.MapMatchMethod); int time = 0; if (position == new Point2f()) { @@ -1213,7 +1217,7 @@ public class PathExecutor await tpTask.OpenBigMapUi(); try { - position =MapManager.GetMap(waypoint.MapName).ConvertGenshinMapCoordinatesToImageCoordinates(tpTask.GetPositionFromBigMap(waypoint.MapName)); + position =MapManager.GetMap(waypoint.MapName, waypoint.MapMatchMethod).ConvertGenshinMapCoordinatesToImageCoordinates(tpTask.GetPositionFromBigMap(waypoint.MapName)); } catch (Exception e) { @@ -1249,6 +1253,16 @@ public class PathExecutor return (position,time); } + private async Task WaitUntilRotatedTo(int targetOrientation, int maxDiff) + { + if (await _rotateTask.WaitUntilRotatedTo(targetOrientation, maxDiff)) + { + return; + } + await ResolveAnomalies(); + await _rotateTask.WaitUntilRotatedTo(targetOrientation, maxDiff); + } + /** * 处理各种异常场景 * 需要保证耗时不能太高 @@ -1264,7 +1278,8 @@ public class PathExecutor var cookRa = imageRegion.Find(AutoSkipAssets.Instance.CookRo); var closeRa = imageRegion.Find(AutoSkipAssets.Instance.PageCloseMainRo); var closeRa2 = imageRegion.Find(ElementAssets.Instance.PageCloseWhiteRo); - if (cookRa.IsExist() || closeRa.IsExist() || closeRa2.IsExist()) + var closeRa3 = imageRegion.Find(AutoSkipAssets.Instance.PageCloseRo); + if (cookRa.IsExist() || closeRa.IsExist() || closeRa2.IsExist() || closeRa3.IsExist()) { // 排除大地图 if (Bv.IsInBigMapUi(imageRegion)) diff --git a/BetterGenshinImpact/GameTask/AutoPathing/PathRecorder.cs b/BetterGenshinImpact/GameTask/AutoPathing/PathRecorder.cs index 97ca4f07..a64f6eb9 100644 --- a/BetterGenshinImpact/GameTask/AutoPathing/PathRecorder.cs +++ b/BetterGenshinImpact/GameTask/AutoPathing/PathRecorder.cs @@ -50,7 +50,8 @@ public class PathRecorder : Singleton public void Start() { - Navigation.WarmUp(); + var matchingMethod = TaskContext.Instance().Config.PathingConditionConfig.MapMatchingMethod; + Navigation.WarmUp(matchingMethod); _pathingTask = new PathingTask(); TaskControl.Logger.LogInformation("开始路径点记录"); if (GetMapName() == nameof(MapTypes.Teyvat)) @@ -60,14 +61,14 @@ public class PathRecorder : Singleton var waypoint = new Waypoint(); var screen = TaskControl.CaptureToRectArea(); - var position = Navigation.GetPositionStable(screen, GetMapName()); + var position = Navigation.GetPositionStable(screen, GetMapName(), matchingMethod); if (position == default) { TaskControl.Logger.LogWarning("未识别到当前位置!"); return; } - position = MapManager.GetMap(GetMapName()).ConvertImageCoordinatesToGenshinMapCoordinates(position); + position = MapManager.GetMap(GetMapName(), matchingMethod).ConvertImageCoordinatesToGenshinMapCoordinates(position); waypoint.X = position.X; waypoint.Y = position.Y; waypoint.Type = WaypointType.Teleport.Code; @@ -88,8 +89,9 @@ public class PathRecorder : Singleton { Waypoint waypoint = new(); var screen = TaskControl.CaptureToRectArea(); - var position = Navigation.GetPositionStable(screen, GetMapName()); - position = MapManager.GetMap(GetMapName()).ConvertImageCoordinatesToGenshinMapCoordinates(position); + var matchingMethod = TaskContext.Instance().Config.PathingConditionConfig.MapMatchingMethod; + var position = Navigation.GetPositionStable(screen, GetMapName(), matchingMethod); + position = MapManager.GetMap(GetMapName(), matchingMethod).ConvertImageCoordinatesToGenshinMapCoordinates(position); if (position == default) { TaskControl.Logger.LogWarning("未识别到当前位置!"); @@ -108,11 +110,13 @@ public class PathRecorder : Singleton { if (_webWindow == null) { + var matchingMethod = TaskContext.Instance().Config.PathingConditionConfig.MapMatchingMethod; _pathingTask.Info = new PathingTaskInfo { Name = "未命名路线", Type = PathingTaskType.Collect.Code, MapName = GetMapName(), + MapMatchMethod = matchingMethod, BgiVersion = Global.Version }; var name = $@"{DateTime.Now:yyyyMMdd_HHmmss}.json"; diff --git a/BetterGenshinImpact/GameTask/AutoPathing/TrapEscaper.cs b/BetterGenshinImpact/GameTask/AutoPathing/TrapEscaper.cs index 94a90d98..fa509647 100644 --- a/BetterGenshinImpact/GameTask/AutoPathing/TrapEscaper.cs +++ b/BetterGenshinImpact/GameTask/AutoPathing/TrapEscaper.cs @@ -35,7 +35,7 @@ public class TrapEscaper(CancellationToken ct) var startTime = DateTime.UtcNow; bool left = false; var screen = CaptureToRectArea(); - var position = Navigation.GetPosition(screen, waypoint.MapName); + var position = Navigation.GetPosition(screen, waypoint.MapName, waypoint.MapMatchMethod); LastActionTime = DateTime.UtcNow; var targetOrientation = Navigation.GetTargetOrientation(waypoint, position); await _rotateTask.WaitUntilRotatedTo(targetOrientation, 5); @@ -56,7 +56,7 @@ public class TrapEscaper(CancellationToken ct) } screen = CaptureToRectArea(); - position = Navigation.GetPosition(screen, waypoint.MapName); + position = Navigation.GetPosition(screen, waypoint.MapName, waypoint.MapMatchMethod); // 旋转视角 /* 这里的角度增加了一个randomAngle角度,用来在原角度不适用的情况下修改角度以适应复杂环境 diff --git a/BetterGenshinImpact/GameTask/AutoPick/AutoPickTrigger.cs b/BetterGenshinImpact/GameTask/AutoPick/AutoPickTrigger.cs index 3bd20911..4e1c1e4a 100644 --- a/BetterGenshinImpact/GameTask/AutoPick/AutoPickTrigger.cs +++ b/BetterGenshinImpact/GameTask/AutoPick/AutoPickTrigger.cs @@ -7,6 +7,7 @@ using BetterGenshinImpact.Core.Simulator; using BetterGenshinImpact.GameTask.AutoPick.Assets; using BetterGenshinImpact.Helpers; using BetterGenshinImpact.Service; +using BetterGenshinImpact.View.Windows; using Microsoft.Extensions.Logging; using OpenCvSharp; using System; @@ -99,7 +100,7 @@ public partial class AutoPickTrigger : ITaskTrigger catch (Exception e) { _logger.LogError(e, "读取拾取黑/白名单失败"); - MessageBox.Error("读取拾取黑/白名单失败,请确认修改后的拾取黑/白名单内容格式是否正确!"); + ThemedMessageBox.Error("读取拾取黑/白名单失败,请确认修改后的拾取黑/白名单内容格式是否正确!"); } return []; @@ -119,7 +120,7 @@ public partial class AutoPickTrigger : ITaskTrigger catch (Exception e) { _logger.LogError(e, "读取拾取黑/白名单失败"); - MessageBox.Error("读取拾取黑/白名单失败,请确认修改后的拾取黑/白名单内容格式是否正确!"); + ThemedMessageBox.Error("读取拾取黑/白名单失败,请确认修改后的拾取黑/白名单内容格式是否正确!"); } return []; @@ -139,7 +140,7 @@ public partial class AutoPickTrigger : ITaskTrigger catch (Exception e) { _logger.LogError(e, "读取拾取黑/白名单失败"); - MessageBox.Error("读取拾取黑/白名单失败,请确认修改后的拾取黑/白名单内容格式是否正确!"); + ThemedMessageBox.Error("读取拾取黑/白名单失败,请确认修改后的拾取黑/白名单内容格式是否正确!"); } return []; diff --git a/BetterGenshinImpact/GameTask/AutoSkip/AutoSkipTrigger.cs b/BetterGenshinImpact/GameTask/AutoSkip/AutoSkipTrigger.cs index f959e351..cf7f9adb 100644 --- a/BetterGenshinImpact/GameTask/AutoSkip/AutoSkipTrigger.cs +++ b/BetterGenshinImpact/GameTask/AutoSkip/AutoSkipTrigger.cs @@ -12,6 +12,7 @@ using BetterGenshinImpact.GameTask.Model.Area; using BetterGenshinImpact.Helpers; using BetterGenshinImpact.Service; using BetterGenshinImpact.View.Drawable; +using BetterGenshinImpact.View.Windows; using Microsoft.Extensions.Logging; using OpenCvSharp; using System; @@ -112,7 +113,7 @@ public partial class AutoSkipTrigger : ITaskTrigger catch (Exception e) { _logger.LogError(e, "读取自动剧情默认暂停点击关键词列表失败"); - MessageBox.Error("读取自动剧情默认暂停点击关键词列表失败,请确认修改后的自动剧情默认暂停点击关键词内容格式是否正确!"); + ThemedMessageBox.Error("读取自动剧情默认暂停点击关键词列表失败,请确认修改后的自动剧情默认暂停点击关键词内容格式是否正确!"); } try @@ -126,7 +127,7 @@ public partial class AutoSkipTrigger : ITaskTrigger catch (Exception e) { _logger.LogError(e, "读取自动剧情暂停点击关键词列表失败"); - MessageBox.Error("读取自动剧情暂停点击关键词列表失败,请确认修改后的自动剧情暂停点击关键词内容格式是否正确!"); + ThemedMessageBox.Error("读取自动剧情暂停点击关键词列表失败,请确认修改后的自动剧情暂停点击关键词内容格式是否正确!"); } try @@ -140,7 +141,7 @@ public partial class AutoSkipTrigger : ITaskTrigger catch (Exception e) { _logger.LogError(e, "读取自动剧情优先点击选项列表失败"); - MessageBox.Error("读取自动剧情优先点击选项列表失败,请确认修改后的自动剧情优先点击选项内容格式是否正确!"); + ThemedMessageBox.Error("读取自动剧情优先点击选项列表失败,请确认修改后的自动剧情优先点击选项内容格式是否正确!"); } } @@ -179,6 +180,8 @@ public partial class AutoSkipTrigger : ITaskTrigger if (_config.ClosePopupPagedEnabled) { ClosePopupPage(content); + CloseItemPopup(content); + CloseCharacterPopup(content); } // 自动剧情点击3s内判断 @@ -712,6 +715,133 @@ public partial class AutoSkipTrigger : ITaskTrigger } }); } + + private DateTime _prevCloseItemTime = DateTime.MinValue; + /// + /// 关闭剧情中弹出的道具页面 + /// + /// + private void CloseItemPopup(CaptureContent content) + { + if ((DateTime.Now - _prevCloseItemTime).TotalMilliseconds < 1000) + { + return; + } + + if (Bv.IsInMainUi(content.CaptureRectArea)) + { + return; + } + //屏幕底部中间,实心三角的位置 + var scale = TaskContext.Instance().SystemInfo.AssetScale; + using var croppedRegion = content.CaptureRectArea.DeriveCrop(900 * scale, 960 * scale, 120 * scale, 120 * scale); + + using var hsv = new Mat(); + Cv2.CvtColor(croppedRegion.SrcMat, hsv, ColorConversionCodes.BGR2HSV); + + using var yellowMask = new Mat(); + using var buleMask = new Mat(); + Cv2.InRange(hsv, new Scalar(0, 222, 173), new Scalar(33, 255, 255), yellowMask); + Cv2.InRange(hsv, new Scalar(87, 131, 142), new Scalar(124, 255, 255), buleMask); //活动玩法介绍会有出现蓝色三角,但不一定在对话流程中出现,先加上 + + Cv2.FindContours(yellowMask, out var yellowContours, out _, RetrievalModes.External, ContourApproximationModes.ApproxSimple); + Cv2.FindContours(buleMask, out var buleMaskContours, out _, RetrievalModes.External, ContourApproximationModes.ApproxSimple); + + var mergedContours = yellowContours.Concat(buleMaskContours).ToArray(); + foreach (var contour in mergedContours) + { + var area = Cv2.ContourArea(contour); + var approx = Cv2.ApproxPolyDP(contour, 0.04 * Cv2.ArcLength(contour, true), true); + + if (area < 10 || area > 50 || approx.Length != 3) continue; + + if (UseBackgroundOperation && !SystemControl.IsGenshinImpactActive()) + { + croppedRegion.Derive(Cv2.BoundingRect(approx)).BackgroundClick(); + } + else + { + croppedRegion.Derive(Cv2.BoundingRect(approx)).Click(); + } + _prevCloseItemTime = DateTime.Now; + _logger.LogInformation("自动剧情:{Text} 面积 {Area}", "点击底部三角形",area); + return; + } + } + + /// + /// 关闭剧情中弹出的初见角色信息弹窗 + /// + /// + private void CloseCharacterPopup(CaptureContent content) + { + using var srcMat = content.CaptureRectArea.SrcMat.Clone(); + var scale = TaskContext.Instance().SystemInfo.AssetScale; + // 把被角色头像遮挡的矩形闭合(假设矩形存在) + Cv2.Rectangle(srcMat, new Rect((int)(240 * scale), (int)(395 * scale), (int)(300 * scale), (int)(50 * scale)), new Scalar(229, 241, 245), -1); + Cv2.Rectangle(srcMat, new Rect((int)(290 * scale), (int)(660 * scale), (int)(210 * scale), (int)(40 * scale)), new Scalar(101, 82, 74), -1); + + using var hsv = new Mat(); + Cv2.CvtColor(srcMat, hsv, ColorConversionCodes.BGR2HSV); + + // 颜色阈值分割 - 背景色中的黄跟藏青 + using var maskLight = new Mat(); + using var maskDark = new Mat(); + Cv2.InRange(hsv, new Scalar(18, 16, 234), new Scalar(27, 19, 250), maskLight); + Cv2.InRange(hsv, new Scalar(101, 57, 95), new Scalar(118, 85, 106), maskDark); + + // 合并掩码并进行形态学操作 - 减少背景中的噪点 + using var combinedMask = new Mat(); + using var kernel = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(21, 21)); + Cv2.BitwiseOr(maskLight, maskDark, combinedMask); + Cv2.MorphologyEx(combinedMask, combinedMask, MorphTypes.Close, kernel); + Cv2.MorphologyEx(combinedMask, combinedMask, MorphTypes.Open, kernel); + + // 查找轮廓 + Cv2.FindContours(combinedMask, out var contours, out _, RetrievalModes.External, ContourApproximationModes.ApproxSimple); + + var imgHeight = srcMat.Height; + var imgWidth = srcMat.Width; + + // 筛选弹窗轮廓 + foreach (var contour in contours) + { + var bbox = Cv2.BoundingRect(contour); + if (bbox.Height == 0) continue; + + // 面积检查 + var areaRatio = (double)(bbox.Width * bbox.Height) / (imgWidth * imgHeight); + if (areaRatio <= 0.24 || areaRatio >= 0.3) continue; // 弹窗高约300,面积比约等于0.27 + _logger.LogDebug("自动剧情:关闭角色弹窗-面积检查通过"); + + // 宽高比检查 + var aspectRatio = (double)bbox.Width / bbox.Height; + if (aspectRatio < 5.6 || aspectRatio > 7.2) continue; + _logger.LogDebug("自动剧情:关闭角色弹窗-宽高比检查通过"); + + // 位置检查 + if (bbox.Y <= imgHeight * 0.3 || bbox.Y + bbox.Height >= imgHeight * 0.7) continue; + _logger.LogDebug("自动剧情:关闭角色弹窗-位置检查通过"); + + + // 检查是否包含两种颜色 + var lightCount = Cv2.CountNonZero(new Mat(maskLight, bbox)); + var darkCount = Cv2.CountNonZero(new Mat(maskDark, bbox)); + if (lightCount <= 0 || darkCount <= 0) continue; + + if (UseBackgroundOperation && !SystemControl.IsGenshinImpactActive()) + { + content.CaptureRectArea.Derive(bbox).BackgroundClick(); + } + else + { + content.CaptureRectArea.ClickTo(100, 100); // 点击角色横幅外的区域才能跳过 + } + + _logger.LogInformation("自动剧情:关闭角色弹窗"); + return; + } + } private bool SubmitGoods(CaptureContent content) { diff --git a/BetterGenshinImpact/GameTask/AutoStygianOnslaught/AutoStygianOnslaughtTask.cs b/BetterGenshinImpact/GameTask/AutoStygianOnslaught/AutoStygianOnslaughtTask.cs index 4b7f919c..653b0627 100644 --- a/BetterGenshinImpact/GameTask/AutoStygianOnslaught/AutoStygianOnslaughtTask.cs +++ b/BetterGenshinImpact/GameTask/AutoStygianOnslaught/AutoStygianOnslaughtTask.cs @@ -26,6 +26,7 @@ using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; +using BetterGenshinImpact.GameTask.AutoDomain; using static BetterGenshinImpact.GameTask.Common.TaskControl; using static Vanara.PInvoke.User32; using BetterGenshinImpact.GameTask.AutoFight; @@ -152,7 +153,7 @@ public class AutoStygianOnslaughtTask : ISoloTask _logger.LogInformation($"{Name}:{{Text}}", "5. 领取奖励"); if (!await GettingTreasure()) { - _logger.LogInformation($"体力耗尽或者设置轮次已达标,{Name}"); + _logger.LogInformation($"{Name}:体力耗尽或者设置轮次已达标"); break; } @@ -323,9 +324,11 @@ public class AutoStygianOnslaughtTask : ISoloTask while (!cts.Token.IsCancellationRequested) { // 通用化战斗策略 - foreach (var command in combatCommands) + for (var i = 0; i < combatCommands.Count; i++) { - command.Execute(combatScenes); + var command = combatCommands[i]; + var lastCommand = i == 0 ? command : combatCommands[i - 1]; + command.Execute(combatScenes, lastCommand); } } } @@ -419,26 +422,24 @@ public class AutoStygianOnslaughtTask : ISoloTask if (resinStatus is { CondensedResinCount: <= 0, OriginalResinCount: < 20 }) { _logger.LogWarning("树脂不足"); - await ExitDomain(); return false; } bool resinUsed = false; if (resinStatus.CondensedResinCount > 0) { - resinUsed = PressUseResin(ra3, "浓缩树脂"); + (resinUsed, _) = AutoDomainTask.PressUseResin(ra3, "浓缩树脂"); resinStatus.CondensedResinCount -= 1; } else if (resinStatus.OriginalResinCount >= 20) { - resinUsed = PressUseResin(ra3, "原粹树脂"); - resinStatus.OriginalResinCount -= 20; + (resinUsed, var num) = AutoDomainTask.PressUseResin(ra3, "原粹树脂"); + resinStatus.OriginalResinCount -= num; } if (!resinUsed) { _logger.LogWarning("自动秘境:未找到可用的树脂,可能是{Msg1} 或者 {Msg2}。", "树脂不足", "OCR 识别失败"); - await ExitDomain(); return false; } @@ -453,18 +454,19 @@ public class AutoStygianOnslaughtTask : ISoloTask // 指定使用树脂 var textListInPrompt2 = ra3.FindMulti(RecognitionObject.Ocr(ra3.Width * 0.25, ra3.Height * 0.2, ra3.Width * 0.5, ra3.Height * 0.6)); // 按优先级使用 - var failCount = 0; + int successCount = 0; foreach (var record in _resinPriorityListWhenSpecifyUse) { - if (record.RemainCount > 0 && PressUseResin(textListInPrompt2, record.Name)) + if (record.RemainCount > 0) { - record.RemainCount -= 1; - _logger.LogInformation("自动秘境:{Name} 刷取 {Re}/{Max}", record.Name, record.MaxCount - record.RemainCount, record.MaxCount); - break; - } - else - { - failCount++; + var (success, _) = AutoDomainTask.PressUseResin(textListInPrompt2, record.Name); + if (success) + { + record.RemainCount -= 1; + Logger.LogInformation("自动秘境:{Name} 刷取 {Re}/{Max}", record.Name, record.MaxCount - record.RemainCount, record.MaxCount); + successCount++; + break; + } } } @@ -474,12 +476,11 @@ public class AutoStygianOnslaughtTask : ISoloTask isLastTurn = true; } - if (failCount == _resinPriorityListWhenSpecifyUse.Count) + if (successCount == 0) { // 没有找到对应的树脂 _logger.LogWarning("自动秘境:指定树脂领取次数时,当前可用树脂选项无法满足配置。你可能设置的刷取次数过多!退出秘境。"); _logger.LogInformation("当前刷取情况:{ResinList}", string.Join(", ", _resinPriorityListWhenSpecifyUse.Select(o => $"{o.Name}({o.MaxCount - o.RemainCount}/{o.MaxCount})"))); - await ExitDomain(); return false; } } @@ -552,62 +553,6 @@ public class AutoStygianOnslaughtTask : ISoloTask await ExitDomain(new BvPage(_ct)); } - private bool PressUseResin(ImageRegion ra, string resinName) - { - var regionList = ra.FindMulti(RecognitionObject.Ocr(ra.Width * 0.25, ra.Height * 0.2, ra.Width * 0.5, ra.Height * 0.6)); - return PressUseResin(regionList, resinName); - } - - private bool PressUseResin(List regionList, string resinName) - { - var resinKey = regionList.FirstOrDefault(t => t.Text.Contains(resinName)); - if (resinKey != null) - { - // 找到树脂名称对应的按键,关键词为使用,是同一行的(高度相交) - var useList = regionList.Where(t => t.Text.Contains("使用")).ToList(); - if (useList.Count != 0) - { - // 找到使用按键 - var useKey = useList.FirstOrDefault(t => t.X > TaskContext.Instance().SystemInfo.ScaleMax1080PCaptureRect.Width / 2 - && IsHeightOverlap(t, resinKey)); - if (useKey != null) - { - // 点击使用 - useKey.Click(); - // 解决水龙王按下左键后没松开,然后后续点击按下就没反应了。使用双击 - Sleep(60, _ct); - useKey.Click(); - _logger.LogInformation("自动秘境:使用 {ResinName}", resinName); - return true; - } - else - { - _logger.LogWarning("自动秘境:未找到 {ResinName} 的使用按键", resinName); - } - } - else - { - _logger.LogWarning("自动秘境:未找到 {ResinName} 的使用按键", resinName); - } - } - - return false; - } - - /// - /// 判断两个区域在垂直方向上是否有重叠 - /// - private bool IsHeightOverlap(Region region1, Region region2) - { - int region1Top = region1.Y; - int region1Bottom = region1.Y + region1.Height; - int region2Top = region2.Y; - int region2Bottom = region2.Y + region2.Height; - - // 检查区域是否在垂直方向上重叠 - return (region1Top <= region2Bottom && region1Bottom >= region2Top); - } - private async Task ArtifactSalvage() { if (!_taskParam.AutoArtifactSalvage) @@ -626,16 +571,25 @@ public class AutoStygianOnslaughtTask : ISoloTask private async Task ExitDomain(BvPage page) { - await Delay(1000, _ct); + + var exitDoor = await NewRetry.WaitForElementAppear( + ElementAssets.Instance.BtnExitDoor.Value, + () => Simulation.SendInput.Keyboard.KeyPress(VK.VK_ESCAPE),// 点击队伍选择按钮 + _ct, + 4, + 1000 + ); + if (exitDoor) + { + await page.Locator(ElementAssets.Instance.BtnExitDoor.Value).Click(); + // 等待传送完成 + await page.Locator(ElementAssets.Instance.PaimonMenuRo).WaitFor(60000); - Simulation.SendInput.Keyboard.KeyPress(VK.VK_ESCAPE); - await Delay(1000, _ct); - - await page.Locator(ElementAssets.Instance.BtnExitDoor.Value).Click(); - - // 等待传送完成 - await page.Locator(ElementAssets.Instance.PaimonMenuRo).WaitFor(60000); - - await Delay(3000, _ct); + await Delay(3000, _ct); + } + else + { + Logger.LogWarning("未能找到退出秘境按钮,可能已经退出秘境"); + } } -} +} \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/AutoTrackPath/Assets/tp.json b/BetterGenshinImpact/GameTask/AutoTrackPath/Assets/tp.json index 621edb83..796d107d 100644 --- a/BetterGenshinImpact/GameTask/AutoTrackPath/Assets/tp.json +++ b/BetterGenshinImpact/GameTask/AutoTrackPath/Assets/tp.json @@ -11777,7 +11777,7 @@ "id": "1555", "gadgetId": "70550055", "gadgetType": "Worktop", - "type": "Worship", + "type": "Others", "position": [ -1913.60009765625, 0, @@ -11792,6 +11792,45 @@ "name": "神庙·煅石之轮", "area": "圣火竞技场" }, + { + "id": "1556", + "gadgetId": "70560001", + "gadgetType": "Worktop", + "type": "Dungeon", + "position": [ + 1730.53564453125, + 0, + -1607.9765625 + ], + "tranPosition": [ + 1730.53515625, + 0, + -1607.9765625 + ], + "country": "蒙德", + "description": "周本", + "name": "待解「弈局」", + "area": "风啸山坡" + }, + { + "id": "1557", + "gadgetId": "70240001", + "gadgetType": "TransPointSecond", + "type": "Others", + "position": [ + -834.4287109375, + 0, + 449.41796875 + ], + "tranPosition": [ + -848.3369140625, + 0, + 417.3955078125 + ], + "country": "璃月", + "name": "群玉阁", + "area": "天衡山" + }, { "id": "1600", "gadgetId": "70580000", @@ -13010,6 +13049,291 @@ "country": "挪德卡莱", "name": "传送锚点", "area": "月矩力试验设计局" + }, + { + "id": "1742", + "gadgetId": "70600001", + "gadgetType": "Worktop", + "type": "MeetingPoint", + "position": [ + 1642.9951171875, + 0, + 9951.4833984375 + ], + "tranPosition": [ + 1640.80419921875, + 0, + 9955.2958984375 + ], + "country": "挪德卡莱", + "name": "「聚所·叮铃哐啷蛋卷工坊」", + "area": "伦波岛" + }, + { + "id": "1743", + "gadgetId": "70600002", + "gadgetType": "Worktop", + "type": "MeetingPoint", + "position": [ + 1817.0732421875, + 0, + 10363.4375 + ], + "tranPosition": [ + 1814.64697265625, + 0, + 10362.52734375 + ], + "country": "挪德卡莱", + "name": "「聚所·霜月之坊」", + "area": "希汐岛" + }, + { + "id": "1744", + "gadgetId": "70600003", + "gadgetType": "Worktop", + "type": "MeetingPoint", + "position": [ + 3714.644287109375, + 0, + 9185.8759765625 + ], + "tranPosition": [ + 3715.861328125, + 0, + 9187.9326171875 + ], + "country": "挪德卡莱", + "name": "「聚所·终夜长茔」", + "area": "帕哈岛" + }, + { + "id": "1745", + "gadgetId": "70600001", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 2251.65478515625, + 0, + 9129.3427734375 + ], + "tranPosition": [ + 2242.04296875, + 0, + 9129.2314453125 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "蓝珀湖" + }, + { + "id": "1746", + "gadgetId": "70600001", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 2137.517822265625, + 0, + 9013.4384765625 + ], + "tranPosition": [ + 2137.094482421875, + 0, + 9021.2490234375 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "伦波岛" + }, + { + "id": "1747", + "gadgetId": "70600001", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 2479.421875, + 0, + 9266.4072265625 + ], + "tranPosition": [ + 2478.599609375, + 0, + 9268.5703125 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "苔骨荒原" + }, + { + "id": "1748", + "gadgetId": "70600002", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 1897.07861328125, + 0, + 10462.408203125 + ], + "tranPosition": [ + 1894.49658203125, + 0, + 10464.185546875 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "「蟹之主的宫殿」" + }, + { + "id": "1749", + "gadgetId": "70600002", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 2017.00390625, + 0, + 11164.1982421875 + ], + "tranPosition": [ + 2020.5087890625, + 0, + 11168.6162109375 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "银月之庭" + }, + { + "id": "1750", + "gadgetId": "70600003", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 3278.798828125, + 0, + 10179.552734375 + ], + "tranPosition": [ + 3268.82958984375, + 0, + 10170.6865234375 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "月矩力试验设计局" + }, + { + "id": "1751", + "gadgetId": "70600003", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 3322.442138671875, + 0, + 10203.83984375 + ], + "tranPosition": [ + 3321.57568359375, + 0, + 10193.837890625 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "月矩力试验设计局" + }, + { + "id": "1752", + "gadgetId": "70600003", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 3345.524169921875, + 0, + 9952.1298828125 + ], + "tranPosition": [ + 3342.756103515625, + 0, + 9958.52734375 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "月矩力试验设计局" + }, + { + "id": "1753", + "gadgetId": "70600003", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 3207.2587890625, + 0, + 9842.6484375 + ], + "tranPosition": [ + 3209.676025390625, + 0, + 9843.7041015625 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "月矩力试验设计局" + }, + { + "id": "1754", + "gadgetId": "70600003", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 3132.601318359375, + 0, + 9911.240234375 + ], + "tranPosition": [ + 3132.36328125, + 0, + 9905.0224609375 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "月矩力试验设计局" + }, + { + "id": "1755", + "gadgetId": "70600003", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 3287.1982421875, + 0, + 9735.75390625 + ], + "tranPosition": [ + 3286.991455078125, + 0, + 9738.3994140625 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "月矩力试验设计局" + }, + { + "id": "1756", + "gadgetId": "70600003", + "gadgetType": "TransPointSecond", + "type": "Teleport", + "position": [ + 3230.959228515625, + 0, + 9930.828125 + ], + "tranPosition": [ + 3229.906982421875, + 0, + 9929.154296875 + ], + "country": "挪德卡莱", + "name": "传送锚点", + "area": "月矩力试验设计局" } ] }, diff --git a/BetterGenshinImpact/GameTask/AutoTrackPath/AutoTrackPathTask.cs b/BetterGenshinImpact/GameTask/AutoTrackPath/AutoTrackPathTask.cs index a18a0622..537f00d0 100644 --- a/BetterGenshinImpact/GameTask/AutoTrackPath/AutoTrackPathTask.cs +++ b/BetterGenshinImpact/GameTask/AutoTrackPath/AutoTrackPathTask.cs @@ -210,7 +210,8 @@ public class AutoTrackPathTask var miniMapMat = GetMiniMapMat(ra) ?? throw new InvalidOperationException("当前不在主界面"); // 注意游戏坐标系的角度是顺时针的 - var currMapImageAvatarPos = MapManager.GetMap(MapTypes.Teyvat).GetMiniMapPosition(miniMapMat); + var matchingMethod = TaskContext.Instance().Config.PathingConditionConfig.MapMatchingMethod; + var currMapImageAvatarPos = MapManager.GetMap(MapTypes.Teyvat, matchingMethod).GetMiniMapPosition(miniMapMat); if (currMapImageAvatarPos.IsEmpty()) { Debug.WriteLine("识别小地图位置失败"); diff --git a/BetterGenshinImpact/GameTask/AutoTrackPath/Model/GiPathPoint.cs b/BetterGenshinImpact/GameTask/AutoTrackPath/Model/GiPathPoint.cs index fd8d7212..0ff36132 100644 --- a/BetterGenshinImpact/GameTask/AutoTrackPath/Model/GiPathPoint.cs +++ b/BetterGenshinImpact/GameTask/AutoTrackPath/Model/GiPathPoint.cs @@ -25,7 +25,8 @@ public class GiPathPoint public static GiPathPoint BuildFrom(Point2f point, int index) { - var pt = MapManager.GetMap(MapTypes.Teyvat).ConvertImageCoordinatesToGenshinMapCoordinates(point); + var matchingMethod = TaskContext.Instance().Config.PathingConditionConfig.MapMatchingMethod; + var pt = MapManager.GetMap(MapTypes.Teyvat, matchingMethod).ConvertImageCoordinatesToGenshinMapCoordinates(point); return new GiPathPoint { Pt = pt, diff --git a/BetterGenshinImpact/GameTask/AutoTrackPath/PathPointRecorder.cs b/BetterGenshinImpact/GameTask/AutoTrackPath/PathPointRecorder.cs index 4c93358f..17ac607f 100644 --- a/BetterGenshinImpact/GameTask/AutoTrackPath/PathPointRecorder.cs +++ b/BetterGenshinImpact/GameTask/AutoTrackPath/PathPointRecorder.cs @@ -54,6 +54,8 @@ public class PathPointRecorder : Singleton public Task RecordTask(CancellationToken ct) { + var matchingMethod = TaskContext.Instance().Config.PathingConditionConfig.MapMatchingMethod; + return new Task(() => { GiPath way = new(); @@ -74,7 +76,7 @@ public class PathPointRecorder : Singleton continue; } - var p2 = MapManager.GetMap(MapTypes.Teyvat).GetMiniMapPosition(new Mat(ra.SrcMat, new Rect(p.X + 24, p.Y - 15, 210, 210))); + var p2 = MapManager.GetMap(MapTypes.Teyvat, matchingMethod).GetMiniMapPosition(new Mat(ra.SrcMat, new Rect(p.X + 24, p.Y - 15, 210, 210))); if (!p2.IsEmpty()) { way.AddPoint(p2); diff --git a/BetterGenshinImpact/GameTask/AutoTrackPath/TpTask.cs b/BetterGenshinImpact/GameTask/AutoTrackPath/TpTask.cs index 1c5a9ffc..748a7648 100644 --- a/BetterGenshinImpact/GameTask/AutoTrackPath/TpTask.cs +++ b/BetterGenshinImpact/GameTask/AutoTrackPath/TpTask.cs @@ -43,6 +43,9 @@ public class TpTask private readonly Rect _captureRect = TaskContext.Instance().SystemInfo.ScaleMax1080PCaptureRect; private readonly double _zoomOutMax1080PRatio = TaskContext.Instance().SystemInfo.ZoomOutMax1080PRatio; private readonly TpConfig _tpConfig = TaskContext.Instance().Config.TpConfig; + private readonly string _mapMatchingMethod = TaskContext.Instance().Config.PathingConditionConfig.MapMatchingMethod; + private readonly BlessingOfTheWelkinMoonTask _blessingOfTheWelkinMoonTask = new(); + private readonly CancellationToken ct; private readonly CultureInfo cultureInfo; private readonly IStringLocalizer stringLocalizer; @@ -116,7 +119,7 @@ public class TpTask Type = WaypointType.Path.Code, MoveMode = MoveModeEnum.Walk.Code }; - var waypointForTrack = new WaypointForTrack(waypoint, MapTypes.Teyvat.ToString()); + var waypointForTrack = new WaypointForTrack(waypoint, nameof(MapTypes.Teyvat), _mapMatchingMethod); await new PathExecutor(ct).MoveTo(waypointForTrack); Simulation.SendInput.SimulateAction(GIActions.Drop); } @@ -334,6 +337,8 @@ public class TpTask //增加容错,小概率情况下碰到,前面点击传送失败 capture.Find(_assets.TeleportButtonRo, rg => rg.Click()); await Delay(delayMs, ct); + // 打开大地图期间推送的月卡会在传送之后直接显示,导致检测不到传送完成。 + await _blessingOfTheWelkinMoonTask.Start(ct); } Logger.LogWarning("传送等待超时,换台电脑吧"); @@ -384,8 +389,8 @@ public class TpTask /// private (double clickX, double clickY) ConvertToGameRegionPosition(string mapName, Rect bigMapInAllMapRect, double x, double y) { - var (picX, picY) = MapManager.GetMap(mapName).ConvertGenshinMapCoordinatesToImageCoordinates((float)x, (float)y); - var picRect = MapManager.GetMap(mapName).ConvertGenshinMapCoordinatesToImageCoordinates(bigMapInAllMapRect); + var (picX, picY) = MapManager.GetMap(mapName, _mapMatchingMethod).ConvertGenshinMapCoordinatesToImageCoordinates((float)x, (float)y); + var picRect = MapManager.GetMap(mapName, _mapMatchingMethod).ConvertGenshinMapCoordinatesToImageCoordinates(bigMapInAllMapRect); Debug.WriteLine($"({picX},{picY}) 在 {picRect} 内,计算它在窗体内的位置"); var clickX = (picX - picRect.X) / picRect.Width * _captureRect.Width; var clickY = (picY - picRect.Y) / picRect.Height * _captureRect.Height; @@ -736,7 +741,7 @@ public class TpTask using var mapScaleButtonRa = ra.Find(QuickTeleportAssets.Instance.MapScaleButtonRo); if (mapScaleButtonRa.IsExist()) { - rect = MapManager.GetMap(mapName).GetBigMapRect(ra.CacheGreyMat); + rect = MapManager.GetMap(mapName, _mapMatchingMethod).GetBigMapRect(ra.CacheGreyMat); if (rect == default) { // 滚轮调整后再次识别 @@ -764,7 +769,7 @@ public class TpTask rect = new Rect(rect.X * s, rect.Y * s, rect.Width * s, rect.Height * s); } - return MapManager.GetMap(mapName).ConvertImageCoordinatesToGenshinMapCoordinates(rect); + return MapManager.GetMap(mapName, _mapMatchingMethod).ConvertImageCoordinatesToGenshinMapCoordinates(rect); } public Point2f GetBigMapCenterPoint(string mapName) @@ -774,7 +779,7 @@ public class TpTask using var mapScaleButtonRa = ra.Find(QuickTeleportAssets.Instance.MapScaleButtonRo); if (mapScaleButtonRa.IsExist()) { - var p = MapManager.GetMap(mapName).GetBigMapPosition(ra.CacheGreyMat); + var p = MapManager.GetMap(mapName, _mapMatchingMethod).GetBigMapPosition(ra.CacheGreyMat); if (p.IsEmpty()) { throw new InvalidOperationException("识别大地图位置失败"); @@ -788,7 +793,7 @@ public class TpTask (x, y) = (p.X * TeyvatMap.BigMap256ScaleTo2048, p.Y * TeyvatMap.BigMap256ScaleTo2048); } - return MapManager.GetMap(mapName).ConvertImageCoordinatesToGenshinMapCoordinates(new Point2f(x, y)); + return MapManager.GetMap(mapName, _mapMatchingMethod).ConvertImageCoordinatesToGenshinMapCoordinates(new Point2f(x, y)); } else { @@ -878,7 +883,7 @@ public class TpTask var list = ra.FindMulti(new RecognitionObject { RecognitionType = RecognitionTypes.Ocr, - RegionOfInterest = new Rect(ra.Width / 2, 0, ra.Width / 2, ra.Height), + RegionOfInterest = new Rect(ra.Width * 2 / 3, 0, ra.Width / 3, ra.Height), ReplaceDictionary = new Dictionary { ["渊下宫"] = ["渊下宮"], diff --git a/BetterGenshinImpact/GameTask/Common/BgiVision/BvStatus.cs b/BetterGenshinImpact/GameTask/Common/BgiVision/BvStatus.cs index 223a0e10..9fc33d17 100644 --- a/BetterGenshinImpact/GameTask/Common/BgiVision/BvStatus.cs +++ b/BetterGenshinImpact/GameTask/Common/BgiVision/BvStatus.cs @@ -139,7 +139,7 @@ public static partial class Bv /// public static bool IsInBigMapUi(ImageRegion captureRa) { - return captureRa.Find(QuickTeleportAssets.Instance.MapScaleButtonRo).IsExist(); + return captureRa.Find(QuickTeleportAssets.Instance.MapScaleButtonRo).IsExist() || captureRa.Find(QuickTeleportAssets.Instance.MapSettingsButtonRo).IsExist(); } /// diff --git a/BetterGenshinImpact/GameTask/Common/Element/Assets/1920x1080/current_avatar_threshold.png b/BetterGenshinImpact/GameTask/Common/Element/Assets/1920x1080/current_avatar_threshold.png new file mode 100644 index 00000000..58e06c0c Binary files /dev/null and b/BetterGenshinImpact/GameTask/Common/Element/Assets/1920x1080/current_avatar_threshold.png differ diff --git a/BetterGenshinImpact/GameTask/Common/Element/Assets/1920x1080/index_1.png b/BetterGenshinImpact/GameTask/Common/Element/Assets/1920x1080/index_1.png new file mode 100644 index 00000000..c06c0dc0 Binary files /dev/null and b/BetterGenshinImpact/GameTask/Common/Element/Assets/1920x1080/index_1.png differ diff --git a/BetterGenshinImpact/GameTask/Common/Element/Assets/1920x1080/index_2.png b/BetterGenshinImpact/GameTask/Common/Element/Assets/1920x1080/index_2.png new file mode 100644 index 00000000..4770a5db Binary files /dev/null and b/BetterGenshinImpact/GameTask/Common/Element/Assets/1920x1080/index_2.png differ diff --git a/BetterGenshinImpact/GameTask/Common/Element/Assets/1920x1080/index_3.png b/BetterGenshinImpact/GameTask/Common/Element/Assets/1920x1080/index_3.png new file mode 100644 index 00000000..994bfc65 Binary files /dev/null and b/BetterGenshinImpact/GameTask/Common/Element/Assets/1920x1080/index_3.png differ diff --git a/BetterGenshinImpact/GameTask/Common/Element/Assets/1920x1080/index_4.png b/BetterGenshinImpact/GameTask/Common/Element/Assets/1920x1080/index_4.png new file mode 100644 index 00000000..f23667f9 Binary files /dev/null and b/BetterGenshinImpact/GameTask/Common/Element/Assets/1920x1080/index_4.png differ diff --git a/BetterGenshinImpact/GameTask/Common/Element/Assets/ElementAssets.cs b/BetterGenshinImpact/GameTask/Common/Element/Assets/ElementAssets.cs index e0e22a6d..aa752832 100644 --- a/BetterGenshinImpact/GameTask/Common/Element/Assets/ElementAssets.cs +++ b/BetterGenshinImpact/GameTask/Common/Element/Assets/ElementAssets.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using BetterGenshinImpact.Core.Recognition; using BetterGenshinImpact.GameTask.Model; using BetterGenshinImpact.Helpers.Extensions; @@ -8,7 +9,7 @@ namespace BetterGenshinImpact.GameTask.Common.Element.Assets; public class ElementAssets : BaseAssets { - public RecognitionObject PromptDialogLeftBottomStar; // 弹出框左下角的星星 + public RecognitionObject PromptDialogLeftBottomStar; // 弹出框左下角的星星 public RecognitionObject BtnWhiteConfirm; public RecognitionObject BtnWhiteCancel; @@ -87,13 +88,33 @@ public class ElementAssets : BaseAssets public RecognitionObject LeylineDisorderIconRo; - private ElementAssets() + public RecognitionObject Index1; + public RecognitionObject Index2; + public RecognitionObject Index3; + public RecognitionObject Index4; + public List IndexList => [Index1, Index2, Index3, Index4]; + public RecognitionObject CurrentAvatarThreshold; + + +#pragma warning disable CS8618 // 在退出构造函数时,不可为 null 的字段必须包含非 null 值。请考虑添加 "required" 修饰符或声明为可为 null。 + private ElementAssets() : base() + { + Initialization(this.systemInfo); + } + + protected ElementAssets(ISystemInfo systemInfo) : base(systemInfo) + { + Initialization(systemInfo); + } +#pragma warning restore CS8618 // 在退出构造函数时,不可为 null 的字段必须包含非 null 值。请考虑添加 "required" 修饰符或声明为可为 null。 + + private void Initialization(ISystemInfo systemInfo) { PromptDialogLeftBottomStar = new RecognitionObject { Name = "PromptDialogLeftBottomStar", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "prompt_dialog_left_bottom_star.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "prompt_dialog_left_bottom_star.png", systemInfo), RegionOfInterest = new Rect(0, CaptureRect.Height / 2, CaptureRect.Width / 2, CaptureRect.Height - CaptureRect.Height / 2), Threshold = 0.8, }.InitTemplate(); @@ -102,58 +123,58 @@ public class ElementAssets : BaseAssets { Name = "BtnWhiteConfirm", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "btn_white_confirm.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "btn_white_confirm.png", systemInfo), Use3Channels = true }.InitTemplate(); BtnWhiteCancel = new RecognitionObject { Name = "BtnWhiteCancel", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "btn_white_cancel.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "btn_white_cancel.png", systemInfo), Use3Channels = true }.InitTemplate(); BtnBlackConfirm = new RecognitionObject { Name = "BtnBlackConfirm", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "btn_black_confirm.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "btn_black_confirm.png", systemInfo), Use3Channels = true }.InitTemplate(); BtnBlackCancel = new RecognitionObject { Name = "BtnBlackCancel", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "btn_black_cancel.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "btn_black_cancel.png", systemInfo), Use3Channels = true }.InitTemplate(); BtnOnlineYes = new RecognitionObject { Name = "BtnOnlineYes", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "btn_online_yes.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "btn_online_yes.png", systemInfo), Use3Channels = true }.InitTemplate(); BtnOnlineNo = new RecognitionObject { Name = "BtnOnlineNo", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "btn_online_no.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "btn_online_no.png", systemInfo), Use3Channels = true }.InitTemplate(); BtnExitDoor = new Lazy(() => new RecognitionObject { Name = "BtnExitDoor", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "btn_exit_door.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "btn_exit_door.png", systemInfo), DrawOnWindow = false }.InitTemplate()); - + // 秘境退出图标 InDomainRo = new RecognitionObject { Name = "InDomain", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "in_domain.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "in_domain.png", systemInfo), RegionOfInterest = new Rect(0, 0, CaptureRect.Width / 4, CaptureRect.Height / 4), DrawOnWindow = false }.InitTemplate(); @@ -164,7 +185,7 @@ public class ElementAssets : BaseAssets { Name = "PaimonMenu", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "paimon_menu.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "paimon_menu.png", systemInfo), RegionOfInterest = new Rect(0, 0, CaptureRect.Width / 4, CaptureRect.Height / 4), DrawOnWindow = false }.InitTemplate(); @@ -174,7 +195,7 @@ public class ElementAssets : BaseAssets { Name = "BlueTrackPoint", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "blue_track_point_28x.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "blue_track_point_28x.png", systemInfo), RegionOfInterest = new Rect((int)(300 * AssetScale), 0, CaptureRect.Width - (int)(600 * AssetScale), CaptureRect.Height), Threshold = 0.6, DrawOnWindow = true @@ -185,7 +206,7 @@ public class ElementAssets : BaseAssets { Name = "UiLeftTopCookIcon", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "ui_left_top_cook_icon.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "ui_left_top_cook_icon.png", systemInfo), RegionOfInterest = new Rect(0, 0, (int)(150 * AssetScale), (int)(120 * AssetScale)), DrawOnWindow = false }.InitTemplate(); @@ -195,7 +216,7 @@ public class ElementAssets : BaseAssets { Name = "SpaceKey", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "key_space.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "key_space.png", systemInfo), RegionOfInterest = new Rect(CaptureRect.Width - (int)(350 * AssetScale), CaptureRect.Height - (int)(70 * AssetScale), (int)(200 * AssetScale), (int)(70 * AssetScale)), DrawOnWindow = false @@ -204,7 +225,7 @@ public class ElementAssets : BaseAssets { Name = "XKey", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "key_x.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "key_x.png", systemInfo), RegionOfInterest = new Rect(CaptureRect.Width - (int)(350 * AssetScale), CaptureRect.Height - (int)(70 * AssetScale), (int)(200 * AssetScale), (int)(70 * AssetScale)), DrawOnWindow = false }.InitTemplate(); @@ -214,7 +235,7 @@ public class ElementAssets : BaseAssets { Name = "FriendChat", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "friend_chat.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "friend_chat.png", systemInfo), RegionOfInterest = new Rect(0, CaptureRect.Height - (int)(70 * AssetScale), (int)(83 * AssetScale), (int)(70 * AssetScale)), DrawOnWindow = false }.InitTemplate(); @@ -224,7 +245,7 @@ public class ElementAssets : BaseAssets { Name = "PartyBtnChooseView", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "party_btn_choose_view.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "party_btn_choose_view.png", systemInfo), RegionOfInterest = new Rect(0, CaptureRect.Height - (int)(120 * AssetScale), CaptureRect.Width / 7, (int)(120 * AssetScale)), DrawOnWindow = false }.InitTemplate(); @@ -232,7 +253,7 @@ public class ElementAssets : BaseAssets { Name = "PartyBtnDelete", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "party_btn_delete.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "party_btn_delete.png", systemInfo), RegionOfInterest = new Rect(CaptureRect.Width / 4, CaptureRect.Height - (int)(120 * AssetScale), CaptureRect.Width / 2, (int)(120 * AssetScale)), DrawOnWindow = false }.InitTemplate(); @@ -242,7 +263,7 @@ public class ElementAssets : BaseAssets { Name = "CraftCondensedResin", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "craft_condensed_resin.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "craft_condensed_resin.png", systemInfo), RegionOfInterest = new Rect(CaptureRect.Width / 2, 0, CaptureRect.Width / 2, CaptureRect.Height / 3 * 2), DrawOnWindow = false }.InitTemplate(); @@ -251,15 +272,15 @@ public class ElementAssets : BaseAssets { Name = "fragileResinCount", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "fragile_resin_count.png"), - RegionOfInterest = new Rect(CaptureRect.Width / 2, CaptureRect.Height * 3/ 4, CaptureRect.Width / 3, CaptureRect.Height / 6), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "fragile_resin_count.png", systemInfo), + RegionOfInterest = new Rect(CaptureRect.Width / 2, CaptureRect.Height * 3 / 4, CaptureRect.Width / 3, CaptureRect.Height / 6), DrawOnWindow = true }.InitTemplate(); CondensedResinCount = new RecognitionObject { Name = "CondensedResinCount", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "condensed_resin_count.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "condensed_resin_count.png", systemInfo), RegionOfInterest = new Rect(CaptureRect.Width / 2, 0, CaptureRect.Width / 4, CaptureRect.Height / 15), DrawOnWindow = true }.InitTemplate(); @@ -268,7 +289,7 @@ public class ElementAssets : BaseAssets { Name = "Keyreduce", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "key_reduce.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "key_reduce.png", systemInfo), RegionOfInterest = new Rect(CaptureRect.Width / 2, CaptureRect.Height / 2, CaptureRect.Width / 2, CaptureRect.Height / 2), DrawOnWindow = false }.InitTemplate(); @@ -277,7 +298,7 @@ public class ElementAssets : BaseAssets { Name = "Keyincrease", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "key_increase.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "key_increase.png", systemInfo), RegionOfInterest = new Rect(CaptureRect.Width / 2, CaptureRect.Height / 2, CaptureRect.Width / 2, CaptureRect.Height / 2), DrawOnWindow = false }.InitTemplate(); @@ -287,7 +308,7 @@ public class ElementAssets : BaseAssets { Name = "BagWeaponUnchecked", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "bag_weapon_unchecked.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "bag_weapon_unchecked.png", systemInfo), RegionOfInterest = CaptureRect.CutTop(0.1), Threshold = 0.87, DrawOnWindow = false @@ -296,7 +317,7 @@ public class ElementAssets : BaseAssets { Name = "BagWeaponChecked", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "bag_weapon_checked.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "bag_weapon_checked.png", systemInfo), RegionOfInterest = CaptureRect.CutTop(0.1), Threshold = 0.8, DrawOnWindow = false @@ -306,7 +327,7 @@ public class ElementAssets : BaseAssets { Name = "BagArtifactUnchecked", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "bag_artifact_unchecked.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "bag_artifact_unchecked.png", systemInfo), RegionOfInterest = CaptureRect.CutTop(0.1), Threshold = 0.87, DrawOnWindow = false @@ -315,7 +336,7 @@ public class ElementAssets : BaseAssets { Name = "BagArtifactChecked", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "bag_artifact_checked.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "bag_artifact_checked.png", systemInfo), RegionOfInterest = CaptureRect.CutTop(0.1), Threshold = 0.8, DrawOnWindow = false @@ -325,7 +346,7 @@ public class ElementAssets : BaseAssets { Name = "BagCharacterDevelopmentItemUnchecked", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "bag_characterdevelopmentitem_unchecked.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "bag_characterdevelopmentitem_unchecked.png", systemInfo), RegionOfInterest = CaptureRect.CutTop(0.1), Threshold = 0.87, DrawOnWindow = false @@ -334,7 +355,7 @@ public class ElementAssets : BaseAssets { Name = "BagCharacterDevelopmentItemChecked", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "bag_characterdevelopmentitem_checked.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "bag_characterdevelopmentitem_checked.png", systemInfo), RegionOfInterest = CaptureRect.CutTop(0.1), Threshold = 0.8, DrawOnWindow = false @@ -344,7 +365,7 @@ public class ElementAssets : BaseAssets { Name = "BagFoodUnchecked", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "bag_food_unchecked.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "bag_food_unchecked.png", systemInfo), RegionOfInterest = CaptureRect.CutTop(0.1), Threshold = 0.87, DrawOnWindow = false @@ -353,7 +374,7 @@ public class ElementAssets : BaseAssets { Name = "BagFoodChecked", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "bag_food_checked.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "bag_food_checked.png", systemInfo), RegionOfInterest = CaptureRect.CutTop(0.1), Threshold = 0.8, DrawOnWindow = false @@ -363,7 +384,7 @@ public class ElementAssets : BaseAssets { Name = "BagMaterialUnchecked", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "bag_material_unchecked.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "bag_material_unchecked.png", systemInfo), RegionOfInterest = CaptureRect.CutTop(0.1), Threshold = 0.87, DrawOnWindow = false @@ -372,7 +393,7 @@ public class ElementAssets : BaseAssets { Name = "BagMaterialChecked", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "bag_material_checked.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "bag_material_checked.png", systemInfo), RegionOfInterest = CaptureRect.CutTop(0.1), Threshold = 0.8, DrawOnWindow = false @@ -382,7 +403,7 @@ public class ElementAssets : BaseAssets { Name = "BagGadgetUnchecked", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "bag_gadget_unchecked.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "bag_gadget_unchecked.png", systemInfo), RegionOfInterest = CaptureRect.CutTop(0.1), Threshold = 0.87, DrawOnWindow = false @@ -391,7 +412,7 @@ public class ElementAssets : BaseAssets { Name = "BagGadgetChecked", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "bag_gadget_checked.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "bag_gadget_checked.png", systemInfo), RegionOfInterest = CaptureRect.CutTop(0.1), Threshold = 0.8, DrawOnWindow = false @@ -401,7 +422,7 @@ public class ElementAssets : BaseAssets { Name = "BagQuestUnchecked", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "bag_quest_unchecked.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "bag_quest_unchecked.png", systemInfo), RegionOfInterest = CaptureRect.CutTop(0.1), Threshold = 0.87, DrawOnWindow = false @@ -410,7 +431,7 @@ public class ElementAssets : BaseAssets { Name = "BagQuestChecked", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "bag_quest_checked.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "bag_quest_checked.png", systemInfo), RegionOfInterest = CaptureRect.CutTop(0.1), Threshold = 0.8, DrawOnWindow = false @@ -420,7 +441,7 @@ public class ElementAssets : BaseAssets { Name = "BagPreciousItemUnchecked", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "bag_preciousitem_unchecked.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "bag_preciousitem_unchecked.png", systemInfo), RegionOfInterest = CaptureRect.CutTop(0.1), Threshold = 0.87, DrawOnWindow = false @@ -429,7 +450,7 @@ public class ElementAssets : BaseAssets { Name = "BagPreciousItemChecked", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "bag_preciousitem_checked.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "bag_preciousitem_checked.png", systemInfo), RegionOfInterest = CaptureRect.CutTop(0.1), Threshold = 0.8, DrawOnWindow = false @@ -439,7 +460,7 @@ public class ElementAssets : BaseAssets { Name = "BagFurnishingUnchecked", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "bag_furnishing_unchecked.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "bag_furnishing_unchecked.png", systemInfo), RegionOfInterest = CaptureRect.CutTop(0.1), Threshold = 0.87, DrawOnWindow = false @@ -448,7 +469,7 @@ public class ElementAssets : BaseAssets { Name = "BagFurnishingChecked", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "bag_furnishing_checked.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "bag_furnishing_checked.png", systemInfo), RegionOfInterest = CaptureRect.CutTop(0.1), Threshold = 0.8, DrawOnWindow = false @@ -459,7 +480,7 @@ public class ElementAssets : BaseAssets { Name = "BtnArtifactSalvage", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "btn_artifact_salvage.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "btn_artifact_salvage.png", systemInfo), RegionOfInterest = CaptureRect.CutBottom(0.1), DrawOnWindow = false }.InitTemplate(); @@ -467,8 +488,8 @@ public class ElementAssets : BaseAssets { Name = "BtnArtifactSalvageConfirm", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "btn_artifact_salvage_confirm.png"), - RegionOfInterest = CaptureRect.CutRightBottom(0.3,0.1), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "btn_artifact_salvage_confirm.png", systemInfo), + RegionOfInterest = CaptureRect.CutRightBottom(0.3, 0.1), DrawOnWindow = false }.InitTemplate(); @@ -477,7 +498,7 @@ public class ElementAssets : BaseAssets { Name = "BtnClaimEncounterPointsRewards", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "btn_claim_encounter_points_rewards.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "btn_claim_encounter_points_rewards.png", systemInfo), RegionOfInterest = CaptureRect.CutRightBottom(0.3, 0.5), DrawOnWindow = false }.InitTemplate(); @@ -486,7 +507,7 @@ public class ElementAssets : BaseAssets { Name = "Primogem", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "primogem.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "primogem.png", systemInfo), RegionOfInterest = new Rect(0, CaptureRect.Height / 3, CaptureRect.Width, CaptureRect.Height / 3), DrawOnWindow = false }.InitTemplate(); @@ -496,7 +517,7 @@ public class ElementAssets : BaseAssets { Name = "EscMailReward", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "esc_mail_reward.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "esc_mail_reward.png", systemInfo), RegionOfInterest = CaptureRect.CutLeftBottom(0.1, 0.5) }.InitTemplate(); @@ -504,7 +525,7 @@ public class ElementAssets : BaseAssets { Name = "Collect", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "collect.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "collect.png", systemInfo), RegionOfInterest = new Rect(0, CaptureRect.Height - CaptureRect.Height / 3, CaptureRect.Width / 4, CaptureRect.Height / 3), DrawOnWindow = false }.InitTemplate(); @@ -513,7 +534,7 @@ public class ElementAssets : BaseAssets { Name = "PageCloseWhite", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "page_close_white.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "page_close_white.png", systemInfo), RegionOfInterest = new Rect(CaptureRect.Width - CaptureRect.Width / 8, 0, CaptureRect.Width / 8, CaptureRect.Height / 8), DrawOnWindow = true }.InitTemplate(); @@ -523,7 +544,7 @@ public class ElementAssets : BaseAssets { Name = "SereniteaPotHome", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "sereniteapot_home.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "sereniteapot_home.png", systemInfo), RegionOfInterest = new Rect(0, 0, CaptureRect.Width, CaptureRect.Height), DrawOnWindow = false }.InitTemplate(); @@ -531,7 +552,7 @@ public class ElementAssets : BaseAssets { Name = "TeleportSereniteaPotHome", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "sereniteapot_home.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "sereniteapot_home.png", systemInfo), RegionOfInterest = new Rect(CaptureRect.Width / 2, CaptureRect.Height / 2, CaptureRect.Width / 2, CaptureRect.Height / 2), DrawOnWindow = false }.InitTemplate(); @@ -539,7 +560,7 @@ public class ElementAssets : BaseAssets { Name = "AYuanIconRo", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "ayuan.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "ayuan.png", systemInfo), RegionOfInterest = new Rect(0, 0, CaptureRect.Width, CaptureRect.Height), DrawOnWindow = false }.InitTemplate(); @@ -549,7 +570,7 @@ public class ElementAssets : BaseAssets { Name = "SereniteaPotLoveRo", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "sereniteapot_love.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "sereniteapot_love.png", systemInfo), RegionOfInterest = new Rect(CaptureRect.Width - CaptureRect.Width / 8, CaptureRect.Height / 2, CaptureRect.Width / 8, CaptureRect.Height / 4), DrawOnWindow = false }.InitTemplate(); @@ -557,7 +578,7 @@ public class ElementAssets : BaseAssets { Name = "SereniteaPotMoneyRo", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "sereniteapot_money.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "sereniteapot_money.png", systemInfo), RegionOfInterest = new Rect(CaptureRect.Width / 2, CaptureRect.Height - CaptureRect.Height / 4, CaptureRect.Width / 4, CaptureRect.Height / 4), DrawOnWindow = false }.InitTemplate(); @@ -565,7 +586,7 @@ public class ElementAssets : BaseAssets { Name = "祝圣精华", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "exp_bottle_big.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "exp_bottle_big.png", systemInfo), RegionOfInterest = new Rect(0, 0, CaptureRect.Width * 7 / 10, CaptureRect.Height), DrawOnWindow = false }.InitTemplate(); @@ -573,7 +594,7 @@ public class ElementAssets : BaseAssets { Name = "祝圣油膏", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "exp_bottle_small.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "exp_bottle_small.png", systemInfo), RegionOfInterest = new Rect(0, 0, CaptureRect.Width * 7 / 10, CaptureRect.Height), DrawOnWindow = false }.InitTemplate(); @@ -581,7 +602,7 @@ public class ElementAssets : BaseAssets { Name = "SereniteapotPageClose", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "sereniteapot_page_close.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "sereniteapot_page_close.png", systemInfo), RegionOfInterest = new Rect(CaptureRect.Width / 2, CaptureRect.Height / 5, CaptureRect.Width / 4, CaptureRect.Height / 8), DrawOnWindow = false }.InitTemplate(); @@ -589,7 +610,7 @@ public class ElementAssets : BaseAssets { Name = "SereniteapotShopNumberBtn", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "sereniteapot_shop_number_btn.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "sereniteapot_shop_number_btn.png", systemInfo), RegionOfInterest = new Rect(CaptureRect.Width / 2, CaptureRect.Height / 2, CaptureRect.Width / 2, CaptureRect.Height / 2), DrawOnWindow = false }.InitTemplate(); @@ -597,7 +618,7 @@ public class ElementAssets : BaseAssets { Name = "大英雄的经验", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "exp_book.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "exp_book.png", systemInfo), RegionOfInterest = new Rect(0, 0, CaptureRect.Width * 7 / 10, CaptureRect.Height), DrawOnWindow = false }.InitTemplate(); @@ -605,7 +626,7 @@ public class ElementAssets : BaseAssets { Name = "流浪者的经验", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "exp_book_small.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "exp_book_small.png", systemInfo), RegionOfInterest = new Rect(0, 0, CaptureRect.Width * 7 / 10, CaptureRect.Height), DrawOnWindow = false }.InitTemplate(); @@ -613,7 +634,7 @@ public class ElementAssets : BaseAssets { Name = "布匹", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "ayuan_cloth.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "ayuan_cloth.png", systemInfo), RegionOfInterest = new Rect(0, 0, CaptureRect.Width * 7 / 10, CaptureRect.Height), DrawOnWindow = false }.InitTemplate(); @@ -621,7 +642,7 @@ public class ElementAssets : BaseAssets { Name = "须臾树脂", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "ayuan_resin.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "ayuan_resin.png", systemInfo), RegionOfInterest = new Rect(0, 0, CaptureRect.Width * 7 / 10, CaptureRect.Height), DrawOnWindow = false }.InitTemplate(); @@ -629,7 +650,7 @@ public class ElementAssets : BaseAssets { Name = "精锻用魔矿", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "ayuan_magicmineralprecision.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "ayuan_magicmineralprecision.png", systemInfo), RegionOfInterest = new Rect(0, 0, CaptureRect.Width * 7 / 10, CaptureRect.Height), DrawOnWindow = false }.InitTemplate(); @@ -637,7 +658,7 @@ public class ElementAssets : BaseAssets { Name = "摩拉", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "ayuan_mola.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "ayuan_mola.png", systemInfo), RegionOfInterest = new Rect(0, 0, CaptureRect.Width * 7 / 10, CaptureRect.Height), DrawOnWindow = false }.InitTemplate(); @@ -645,19 +666,63 @@ public class ElementAssets : BaseAssets { Name = "尘歌壶小手", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "finger.png"), - RegionOfInterest = new Rect(CaptureRect.Width - (int)(650*AssetScale), 0, (int)(80 * AssetScale), (int)(80 * AssetScale)), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "finger.png", systemInfo), + RegionOfInterest = new Rect(CaptureRect.Width - (int)(600 * AssetScale), 0, (int)(80 * AssetScale), (int)(80 * AssetScale)), DrawOnWindow = false }.InitTemplate(); LeylineDisorderIconRo = new RecognitionObject { Name = "LeylineDisorderIcon", RecognitionType = RecognitionTypes.TemplateMatch, - TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "leyline_disorder_icon.png"), + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "leyline_disorder_icon.png", systemInfo), RegionOfInterest = new Rect(0, 0, (int)(200 * AssetScale), (int)(200 * AssetScale)), DrawOnWindow = false }.InitTemplate(); + Rect partyRect = new Rect(CaptureRect.Width - (int)(65 * AssetScale), (int)(155 * AssetScale), (int)(35 * AssetScale), (int)(600 * AssetScale)); + // 1 2 3 4 按键 + Index1 = new RecognitionObject + { + Name = "Index1", + RecognitionType = RecognitionTypes.TemplateMatch, + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "index_1.png", systemInfo), + RegionOfInterest = partyRect, + // DrawOnWindow = true + }.InitTemplate(); + Index2 = new RecognitionObject + { + Name = "Index2", + RecognitionType = RecognitionTypes.TemplateMatch, + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "index_2.png", systemInfo), + RegionOfInterest = partyRect, + // DrawOnWindow = true + }.InitTemplate(); + Index3 = new RecognitionObject + { + Name = "Index3", + RecognitionType = RecognitionTypes.TemplateMatch, + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "index_3.png", systemInfo), + RegionOfInterest = partyRect, + // DrawOnWindow = true + }.InitTemplate(); + Index4 = new RecognitionObject + { + Name = "Index4", + RecognitionType = RecognitionTypes.TemplateMatch, + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "index_4.png", systemInfo), + RegionOfInterest = partyRect, + // DrawOnWindow = true + }.InitTemplate(); + CurrentAvatarThreshold = new RecognitionObject + { + Name = "CurrentAvatarThreshold", + RecognitionType = RecognitionTypes.TemplateMatch, + TemplateImageMat = GameTaskManager.LoadAssetImage(@"Common\Element", "current_avatar_threshold.png", systemInfo), + RegionOfInterest = new Rect(CaptureRect.Width - (int)(240 * AssetScale), (int)(155 * AssetScale), (int)(210 * AssetScale), (int)(600 * AssetScale)), + UseBinaryMatch = true, + BinaryThreshold = 200, + // DrawOnWindow = true + }.InitTemplate(); } } \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/Common/Element/Assets/MapLazyAssets.cs b/BetterGenshinImpact/GameTask/Common/Element/Assets/MapLazyAssets.cs index 6fdeb7cb..afe713da 100644 --- a/BetterGenshinImpact/GameTask/Common/Element/Assets/MapLazyAssets.cs +++ b/BetterGenshinImpact/GameTask/Common/Element/Assets/MapLazyAssets.cs @@ -25,6 +25,7 @@ public class MapLazyAssets : Singleton { "须弥", [2877, -374] }, { "枫丹", [4515, 3631] }, { "纳塔", [8973.5, -1879.1] }, + { "挪德卡莱", [9542.25, 1661.84] }, }; public readonly Dictionary DomainPositionMap = new(); diff --git a/BetterGenshinImpact/GameTask/Common/Job/BlessingOfTheWelkinMoonTask.cs b/BetterGenshinImpact/GameTask/Common/Job/BlessingOfTheWelkinMoonTask.cs index 1b58fdf7..88ce3beb 100644 --- a/BetterGenshinImpact/GameTask/Common/Job/BlessingOfTheWelkinMoonTask.cs +++ b/BetterGenshinImpact/GameTask/Common/Job/BlessingOfTheWelkinMoonTask.cs @@ -3,6 +3,7 @@ using System; using System.Threading; using System.Threading.Tasks; using BetterGenshinImpact.GameTask.Model.Area; +using BetterGenshinImpact.GameTask.Common.Element.Assets; using BetterGenshinImpact.Helpers; using Microsoft.Extensions.Logging; using static BetterGenshinImpact.GameTask.Common.TaskControl; @@ -20,31 +21,41 @@ public class BlessingOfTheWelkinMoonTask { try { - // 4点全程触发 - if (ServerTimeHelper.GetServerTimeNow().Hour == 4) + var t = ServerTimeHelper.GetServerTimeNow().AddMinutes(5); + if (t.Hour == 4 && t.Minute < 10) { using var ra = CaptureToRectArea(); if (Bv.IsInBlessingOfTheWelkinMoon(ra)) { Logger.LogInformation("检测到空月祝福界面,自动点击"); - GameCaptureRegion.GameRegion1080PPosMove(100,100); - TaskContext.Instance().PostMessageSimulator.LeftButtonClickBackground(); - await Delay(5000, ct); - - // 重新判断一次,因为界面刚出来的点击可能无效 - if (Bv.IsInBlessingOfTheWelkinMoon(ra)) + GameCaptureRegion.GameRegion1080PPosMove(100, 100); + for (int i = 0, j = 0; i < 20 && j < 3; ++i) { - TaskContext.Instance().PostMessageSimulator.LeftButtonClickBackground(); - await Delay(5000, ct); + if (j == 0) + { + // 双击快速跳过 + TaskContext.Instance().PostMessageSimulator.LeftButtonClickBackground(); + TaskContext.Instance().PostMessageSimulator.LeftButtonClickBackground(); + } + await Delay(500, ct); + using var ra2 = CaptureToRectArea(); + if (Bv.IsInBlessingOfTheWelkinMoon(ra2)) + { + // 仍在空月祝福界面 + j = 0; + } + else if (ra2.Find(ElementAssets.Instance.PrimogemRo).IsExist()) + { + // 仍在原石界面 + j = 0; + } + else + { + // 连续3次没检测到才认为处理完毕,避免淡出/淡入特效影响 + ++j; + } } - - await Delay(2000, ct); - - TaskContext.Instance().PostMessageSimulator.LeftButtonClickBackground(); - - await Delay(2000, ct); - - TaskContext.Instance().PostMessageSimulator.LeftButtonClickBackground(); + Logger.LogInformation("空月祝福处理完毕"); } } } diff --git a/BetterGenshinImpact/GameTask/Common/Job/CountInventoryItem.cs b/BetterGenshinImpact/GameTask/Common/Job/CountInventoryItem.cs index 6dd49f51..82fe8218 100644 --- a/BetterGenshinImpact/GameTask/Common/Job/CountInventoryItem.cs +++ b/BetterGenshinImpact/GameTask/Common/Job/CountInventoryItem.cs @@ -4,6 +4,7 @@ using BetterGenshinImpact.GameTask.AutoArtifactSalvage; using BetterGenshinImpact.GameTask.GetGridIcons; using BetterGenshinImpact.GameTask.Model.Area; using BetterGenshinImpact.GameTask.Model.GameUI; +using BetterGenshinImpact.View.Drawable; using Fischless.WindowsInput; using Microsoft.Extensions.Logging; using Microsoft.ML.OnnxRuntime; @@ -82,31 +83,41 @@ namespace BetterGenshinImpact.GameTask.Common.Job private async Task FindOne(InferenceSession session, Dictionary prototypes) { GridScreen gridScreen = new GridScreen(GridParams.Templates[this.gridScreenName], logger, ct); + gridScreen.OnAfterTurnToNewPage += GridScreen.DrawItemsAfterTurnToNewPage; + gridScreen.OnBeforeScroll += () => VisionContext.Instance().DrawContent.ClearAll(); int? count = null; - await foreach (ImageRegion itemRegion in gridScreen) + try { - using Mat icon = itemRegion.SrcMat.GetGridIcon(); - var result = GridIconsAccuracyTestTask.Infer(icon, session, prototypes); - if (result.Item1 == null) + await foreach ((ImageRegion pageRegion, Rect itemRect) in gridScreen) { - continue; - } - string predName = result.Item1; - if (predName == this.itemName!) - { - string numStr = itemRegion.SrcMat.GetGridItemIconText(OcrFactory.Paddle); - if (int.TryParse(numStr, out int num)) + using ImageRegion itemRegion = pageRegion.DeriveCrop(itemRect); + using Mat icon = itemRegion.SrcMat.GetGridIcon(); + var result = GridIconsAccuracyTestTask.Infer(icon, session, prototypes); + if (result.Item1 == null) { - count = num; + continue; } - else + string predName = result.Item1; + if (predName == this.itemName!) { - logger.LogWarning("无法识别数量:{text}", numStr); - count = -2; + string numStr = itemRegion.SrcMat.GetGridItemIconText(OcrFactory.Paddle); + if (int.TryParse(numStr, out int num)) + { + count = num; + } + else + { + logger.LogWarning("无法识别数量:{text}", numStr); + count = -2; + } + break; } - break; } } + finally + { + VisionContext.Instance().DrawContent.ClearAll(); + } if (count == null) { count = -1; @@ -121,42 +132,52 @@ namespace BetterGenshinImpact.GameTask.Common.Job List notFoundItemNames = this.itemNames!.ToList(); GridScreen gridScreen = new GridScreen(GridParams.Templates[this.gridScreenName], logger, ct); - await foreach (ImageRegion itemRegion in gridScreen) + gridScreen.OnAfterTurnToNewPage += GridScreen.DrawItemsAfterTurnToNewPage; + gridScreen.OnBeforeScroll += () => VisionContext.Instance().DrawContent.ClearAll(); + try { - using Mat icon = itemRegion.SrcMat.GetGridIcon(); - var result = GridIconsAccuracyTestTask.Infer(icon, session, prototypes); - if (result.Item1 == null) + await foreach ((ImageRegion pageRegion, Rect itemRect) in gridScreen) { - continue; - } - string predName = result.Item1; - if (this.itemNames!.Contains(predName) && !itemsCountDic!.ContainsKey(predName)) - { - int count; - string numStr = itemRegion.SrcMat.GetGridItemIconText(OcrFactory.Paddle); - if (int.TryParse(numStr, out int num)) + using ImageRegion itemRegion = pageRegion.DeriveCrop(itemRect); + using Mat icon = itemRegion.SrcMat.GetGridIcon(); + var result = GridIconsAccuracyTestTask.Infer(icon, session, prototypes); + if (result.Item1 == null) { - count = num; + continue; } - else + string predName = result.Item1; + if (this.itemNames!.Contains(predName) && !itemsCountDic!.ContainsKey(predName)) { - logger.LogWarning("无法识别数量:{text}", numStr); - count = -2; - } + int count; + string numStr = itemRegion.SrcMat.GetGridItemIconText(OcrFactory.Paddle); + if (int.TryParse(numStr, out int num)) + { + count = num; + } + else + { + logger.LogWarning("无法识别数量:{text}", numStr); + count = -2; + } - if (!itemsCountDic!.TryAdd(predName, count)) - { - logger.LogWarning("重复的名称:{name}", predName); - } + if (!itemsCountDic!.TryAdd(predName, count)) + { + logger.LogWarning("重复的名称:{name}", predName); + } - notFoundItemNames.RemoveAll(n => n == predName); + notFoundItemNames.RemoveAll(n => n == predName); - if (notFoundItemNames.Count <= 0) - { - break; + if (notFoundItemNames.Count <= 0) + { + break; + } } } } + finally + { + VisionContext.Instance().DrawContent.ClearAll(); + } if (notFoundItemNames.Count > 0) { diff --git a/BetterGenshinImpact/GameTask/Common/Job/GoToSereniteaPotTask.cs b/BetterGenshinImpact/GameTask/Common/Job/GoToSereniteaPotTask.cs index b618d601..6299a9f0 100644 --- a/BetterGenshinImpact/GameTask/Common/Job/GoToSereniteaPotTask.cs +++ b/BetterGenshinImpact/GameTask/Common/Job/GoToSereniteaPotTask.cs @@ -390,19 +390,19 @@ internal class GoToSereniteaPotTask } Logger.LogInformation("领取尘歌壶奖励:{text}", "购买商店物品最大数量"); - var numberBtn = ra.Find(ElementAssets.Instance.SereniteapotShopNumberBtn); - if (numberBtn.IsExist()) - { - numberBtn.Move(); - await Delay(600, ct);//减慢速度,设备差异导致的延迟 - Simulation.SendInput.Mouse.LeftButtonDown(); - await Delay(600, ct); - numberBtn.MoveTo(ra.Width/7,0);//moveby会超出边界,改用MoveTo - await Delay(600, ct); - Simulation.SendInput.Mouse.LeftButtonUp(); - } + // var numberBtn = ra.Find(ElementAssets.Instance.SereniteapotShopNumberBtn); + // if (numberBtn.IsExist()) + // { + // numberBtn.Move(); + // await Delay(600, ct);//减慢速度,设备差异导致的延迟 + // Simulation.SendInput.Mouse.LeftButtonDown(); + // await Delay(600, ct); + // numberBtn.MoveTo(ra.Width/7,0);//moveby会超出边界,改用MoveTo + // await Delay(600, ct); + // Simulation.SendInput.Mouse.LeftButtonUp(); + // } - await Delay(600, ct); + // await Delay(600, ct); ra.Find(ElementAssets.Instance.BtnWhiteConfirm).Click(); await Delay(600, ct); TaskContext.Instance().PostMessageSimulator.SimulateAction(GIActions.OpenPaimonMenu); // ESC diff --git a/BetterGenshinImpact/GameTask/Common/Job/ReturnMainUiTask.cs b/BetterGenshinImpact/GameTask/Common/Job/ReturnMainUiTask.cs index a6f4a223..3dc2423e 100644 --- a/BetterGenshinImpact/GameTask/Common/Job/ReturnMainUiTask.cs +++ b/BetterGenshinImpact/GameTask/Common/Job/ReturnMainUiTask.cs @@ -1,7 +1,9 @@ using System.Threading; using System.Threading.Tasks; +using BetterGenshinImpact.Core.BgiVision; using BetterGenshinImpact.Core.Simulator; using BetterGenshinImpact.GameTask.Common.BgiVision; +using BetterGenshinImpact.GameTask.Common.Element.Assets; using Vanara.PInvoke; using static BetterGenshinImpact.GameTask.Common.TaskControl; @@ -21,11 +23,27 @@ public class ReturnMainUiTask for (var i = 0; i < 8; i++) { Simulation.SendInput.Keyboard.KeyPress(User32.VK.VK_ESCAPE); - await Delay(1000, ct); - if (Bv.IsInMainUi(CaptureToRectArea())) + await Delay(900, ct); + + var region = CaptureToRectArea(); + + var exitDoor = region.Find(ElementAssets.Instance.BtnExitDoor.Value); + if (exitDoor.IsExist()) { + exitDoor.Click(); + await Delay(5000, ct); + region = CaptureToRectArea(); + } + + if (Bv.IsInMainUi(region)) + { + region.Dispose(); return; } + else + { + region.Dispose(); + } } await Delay(500, ct); Simulation.SendInput.Keyboard.KeyPress(User32.VK.VK_RETURN); diff --git a/BetterGenshinImpact/GameTask/Common/Job/SetTimeTask.cs b/BetterGenshinImpact/GameTask/Common/Job/SetTimeTask.cs index 49fc3e61..a8524d5b 100644 --- a/BetterGenshinImpact/GameTask/Common/Job/SetTimeTask.cs +++ b/BetterGenshinImpact/GameTask/Common/Job/SetTimeTask.cs @@ -61,11 +61,13 @@ public class SetTimeTask if (skipTimeAdjustmentAnimation) { // 跳过调整动画 - await Delay(1, ct); + await Delay(10, ct); await CancelAnimation(ct); - await Delay(200, ct); + await Delay(1010, ct); GameCaptureRegion.GameRegion1080PPosClick(45, 715); - await Delay(400, ct); + await Delay(100, ct); + GameCaptureRegion.GameRegion1080PPosClick(45, 715); + await Delay(200, ct); await _returnMainUiTask.Start(ct); // 跳过动画不总能成功 if (Bv.IsInMainUi(CaptureToRectArea())) @@ -82,10 +84,9 @@ public class SetTimeTask // 取消动画函数 private async Task CancelAnimation(CancellationToken ct) { - GameCaptureRegion.GameRegion1080PPosMove(200, 200); - Simulation.SendInput.Mouse.LeftButtonDown(); - await Delay(10, ct); - Simulation.SendInput.Mouse.LeftButtonUp(); + GameCaptureRegion.GameRegion1080PPosClick(200, 200); + await Delay(5, ct); + GameCaptureRegion.GameRegion1080PPosClick(200, 200); } double[] GetPosition(double r, double index) diff --git a/BetterGenshinImpact/GameTask/Common/Job/SwitchPartyTask.cs b/BetterGenshinImpact/GameTask/Common/Job/SwitchPartyTask.cs index cbaa2881..3e02d535 100644 --- a/BetterGenshinImpact/GameTask/Common/Job/SwitchPartyTask.cs +++ b/BetterGenshinImpact/GameTask/Common/Job/SwitchPartyTask.cs @@ -151,7 +151,7 @@ public class SwitchPartyTask RegionOfInterest = regionOfInterest, DrawOnWindow = true, Name = "队伍名称", - DrawOnWindowPen= new System.Drawing.Pen(System.Drawing.Color.White) + DrawOnWindowPen= System.Drawing.Pens.White }; // 逐页查找 try diff --git a/BetterGenshinImpact/GameTask/Common/Map/Maps/MapManager.cs b/BetterGenshinImpact/GameTask/Common/Map/Maps/MapManager.cs index c9b9a2c8..c9c87978 100644 --- a/BetterGenshinImpact/GameTask/Common/Map/Maps/MapManager.cs +++ b/BetterGenshinImpact/GameTask/Common/Map/Maps/MapManager.cs @@ -8,7 +8,7 @@ public static class MapManager private static readonly Dictionary _maps = new(); private static readonly object LockObject = new(); - public static ISceneMap GetMap(string mapName, string? matchingMethod = null) + public static ISceneMap GetMap(string mapName, string matchingMethod) { return GetMap(MapTypesExtensions.ParseFromName(mapName), matchingMethod); } @@ -20,15 +20,14 @@ public static class MapManager /// 地图类型 /// 地图匹配方式 /// 地图实例 - public static ISceneMap GetMap(MapTypes mapType, string? matchingMethod = null) + public static ISceneMap GetMap(MapTypes mapType, string matchingMethod) { - if (matchingMethod == null) + if (string.IsNullOrEmpty(matchingMethod)) { matchingMethod = TaskContext.Instance().Config.PathingConditionConfig.MapMatchingMethod; } - string key = $"{mapType}_{matchingMethod}"; - + if (_maps.TryGetValue(key, out var map)) { return map; diff --git a/BetterGenshinImpact/GameTask/GetGridIcons/GetGridIconsTask.cs b/BetterGenshinImpact/GameTask/GetGridIcons/GetGridIconsTask.cs index 3ac3678d..3979c8bb 100644 --- a/BetterGenshinImpact/GameTask/GetGridIcons/GetGridIconsTask.cs +++ b/BetterGenshinImpact/GameTask/GetGridIcons/GetGridIconsTask.cs @@ -7,6 +7,7 @@ using BetterGenshinImpact.GameTask.Model; using BetterGenshinImpact.GameTask.Model.Area; using BetterGenshinImpact.GameTask.Model.GameUI; using BetterGenshinImpact.Helpers.Extensions; +using BetterGenshinImpact.View.Drawable; using Fischless.WindowsInput; using Microsoft.Extensions.Logging; using OpenCvSharp; @@ -83,65 +84,76 @@ public class GetGridIconsTask : ISoloTask private async Task GetInventoryGridIcons(int count, string directory) { GridScreen gridScreen = new GridScreen(GridParams.Templates[this.gridScreenName], this.logger, this.ct); + gridScreen.OnAfterTurnToNewPage += GridScreen.DrawItemsAfterTurnToNewPage; + gridScreen.OnBeforeScroll += () => VisionContext.Instance().DrawContent.ClearAll(); HashSet fileNames = new HashSet(); - await foreach (ImageRegion itemRegion in gridScreen) + try { - itemRegion.Click(); - await Delay(300, ct); - - using var ra1 = CaptureToRectArea(); - using ImageRegion nameRegion = ra1.DeriveCrop(new Rect((int)(ra1.Width * 0.682), (int)(ra1.Width * 0.0625), (int)(ra1.Width * 0.256), (int)(ra1.Width * 0.03125))); - var ocrResult = OcrFactory.Paddle.OcrResult(nameRegion.SrcMat); - string itemName = ocrResult.Text; - string itemStar = ""; - if (this.starAsSuffix) + await foreach ((ImageRegion pageRegion, Rect itemRect) in gridScreen) { - using ImageRegion starRegion = ra1.DeriveCrop(new Rect((int)(ra1.Width * 0.682), (int)(ra1.Width * 0.1823), (int)(ra1.Width * 0.105), (int)(ra1.Width * 0.02345))); - itemStar = String.Join(string.Empty, Enumerable.Repeat("★", GetStars(starRegion.SrcMat))); - } + using ImageRegion itemRegion = pageRegion.DeriveCrop(itemRect); + itemRegion.Click(); + await Delay(300, ct); - string fileName = itemName + itemStar; - if (fileNames.Add(fileName)) - { - string filePath = Path.Combine(directory, $"{fileName}.png"); - Thread saveThread = new Thread(() => + using var ra1 = CaptureToRectArea(); + using ImageRegion nameRegion = ra1.DeriveCrop(new Rect((int)(ra1.Width * 0.682), (int)(ra1.Width * 0.0625), (int)(ra1.Width * 0.256), (int)(ra1.Width * 0.03125))); + var ocrResult = OcrFactory.Paddle.OcrResult(nameRegion.SrcMat); + string itemName = ocrResult.Text; + string itemStar = ""; + if (this.starAsSuffix) { - try - { - using (FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None)) - { - itemRegion.SrcMat.ToBitmap().Save(fs, System.Drawing.Imaging.ImageFormat.Png); - } - logger.LogInformation("图片保存成功:{Text}", fileName); - } - catch (Exception e) - { - logger.LogError(e, "图片保存失败:{Text}", fileName); - } - }); - saveThread.IsBackground = true; // 设置为后台线程 - saveThread.Start(); - } - else - { - logger.LogInformation("重复的物品:{Text}", fileName); - } + using ImageRegion starRegion = ra1.DeriveCrop(new Rect((int)(ra1.Width * 0.682), (int)(ra1.Width * 0.1823), (int)(ra1.Width * 0.105), (int)(ra1.Width * 0.02345))); + itemStar = String.Join(string.Empty, Enumerable.Repeat("★", GetStars(starRegion.SrcMat))); + } - count--; - if (count <= 0) - { - logger.LogInformation("检查次数已耗尽"); - break; + string fileName = itemName + itemStar; + if (fileNames.Add(fileName)) + { + string filePath = Path.Combine(directory, $"{fileName}.png"); + Thread saveThread = new Thread(() => + { + try + { + using (FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None)) + { + itemRegion.SrcMat.ToBitmap().Save(fs, System.Drawing.Imaging.ImageFormat.Png); + } + logger.LogInformation("图片保存成功:{Text}", fileName); + } + catch (Exception e) + { + logger.LogError(e, "图片保存失败:{Text}", fileName); + } + }); + saveThread.IsBackground = true; // 设置为后台线程 + saveThread.Start(); + } + else + { + logger.LogInformation("重复的物品:{Text}", fileName); + } + + count--; + if (count <= 0) + { + logger.LogInformation("检查次数已耗尽"); + break; + } } } + finally + { + VisionContext.Instance().DrawContent.ClearAll(); + } } private async Task GetArtifactSetFilterGridIcons(int count, string directory) { ArtifactSetFilterScreen gridScreen = new ArtifactSetFilterScreen(new GridParams(new Rect(40, 100, 1300, 852), 2, 3, 40, 40, 0.024), this.logger, this.ct); HashSet fileNames = new HashSet(); - await foreach (ImageRegion itemRegion in gridScreen) + await foreach ((ImageRegion pageRegion, Rect itemRect) in gridScreen) { + using ImageRegion itemRegion = pageRegion.DeriveCrop(itemRect); itemRegion.Click(); await Delay(300, ct); @@ -165,7 +177,7 @@ public class GetGridIconsTask : ISoloTask // 截取没有符号的区域再识别一次 Rect flowerWithoutGlyph = new Rect((int)(ra1.Width * 0.028), (int)(flowerWithGlyphRect.Y - flowerWithGlyphRect.Height * 0), (int)(ra1.Width * 0.228), (int)(flowerWithGlyphRect.Height * 1)); - Mat roi = nameRegion.SrcMat.SubMat(flowerWithoutGlyph); + using Mat roi = nameRegion.SrcMat.SubMat(flowerWithoutGlyph); var whiteOcrResult = OcrFactory.Paddle.OcrResult(roi); flowerName = whiteOcrResult.Text; // 所以只好识别两次,Trim后根据字数取原截图OCR的结果…… @@ -232,7 +244,7 @@ public class GetGridIconsTask : ISoloTask double width = 60; double height = 60; // 宽高缩放似乎不一致,似乎在2.05:2.15之间,但不知道怎么测定 Rect iconRect = new Rect((int)(itemRegion.Width / 2 - 237 * scale - width / 2), (int)(itemRegion.Height / 2 - height / 2), (int)width, (int)height); - Mat crop = itemRegion.SrcMat.SubMat(iconRect); + using Mat crop = itemRegion.SrcMat.SubMat(iconRect); return crop.Resize(new Size(125, 125)); } diff --git a/BetterGenshinImpact/GameTask/GetGridIcons/GridIconsAccuracyTestTask.cs b/BetterGenshinImpact/GameTask/GetGridIcons/GridIconsAccuracyTestTask.cs index 4e5a9e22..08e20ac6 100644 --- a/BetterGenshinImpact/GameTask/GetGridIcons/GridIconsAccuracyTestTask.cs +++ b/BetterGenshinImpact/GameTask/GetGridIcons/GridIconsAccuracyTestTask.cs @@ -1,3 +1,4 @@ +using BetterGenshinImpact.Core.Config; using BetterGenshinImpact.Core.Recognition.OCR; using BetterGenshinImpact.Core.Simulator; using BetterGenshinImpact.GameTask.AutoArtifactSalvage; @@ -5,6 +6,7 @@ using BetterGenshinImpact.GameTask.Common.Job; using BetterGenshinImpact.GameTask.Model.Area; using BetterGenshinImpact.GameTask.Model.GameUI; using BetterGenshinImpact.Helpers.Extensions; +using BetterGenshinImpact.View.Drawable; using Fischless.WindowsInput; using Microsoft.Extensions.Logging; using Microsoft.ML.OnnxRuntime; @@ -16,7 +18,6 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -using BetterGenshinImpact.Core.Config; using static BetterGenshinImpact.GameTask.Common.TaskControl; namespace BetterGenshinImpact.GameTask.GetGridIcons; @@ -108,55 +109,65 @@ public class GridIconsAccuracyTestTask : ISoloTask double total_count = 0; GridScreen gridScreen = new GridScreen(GridParams.Templates[this.gridScreenName], this.logger, this.ct); - await foreach (ImageRegion itemRegion in gridScreen) + gridScreen.OnAfterTurnToNewPage += GridScreen.DrawItemsAfterTurnToNewPage; + gridScreen.OnBeforeScroll += () => VisionContext.Instance().DrawContent.ClearAll(); + try { - itemRegion.Click(); - Task task1 = Delay(300, ct); - - // 用模型推理得到的结果 - Task<(string?, int)> task2 = Task.Run(() => + await foreach ((ImageRegion pageRegion, Rect itemRect) in gridScreen) { - using Mat icon = itemRegion.SrcMat.GetGridIcon(); - return Infer(icon, session, prototypes); - }, ct); + using ImageRegion itemRegion = pageRegion.DeriveCrop(itemRect); + itemRegion.Click(); + Task task1 = Delay(300, ct); - await Task.WhenAll(task1, task2); - (string?, int) result = task2.Result; - string? predName = result.Item1; - int predStarNum = result.Item2; + // 用模型推理得到的结果 + Task<(string?, int)> task2 = Task.Run(() => + { + using Mat icon = itemRegion.SrcMat.GetGridIcon(); + return Infer(icon, session, prototypes); + }, ct); - // 用CV方法得到的结果 - using var ra1 = CaptureToRectArea(); - using ImageRegion nameRegion = ra1.DeriveCrop(new Rect((int)(ra1.Width * 0.682), (int)(ra1.Width * 0.0625), (int)(ra1.Width * 0.256), (int)(ra1.Width * 0.03125))); - var ocrResult = OcrFactory.Paddle.OcrResult(nameRegion.SrcMat); - string itemName = ocrResult.Text; + await Task.WhenAll(task1, task2); + (string?, int) result = task2.Result; + string? predName = result.Item1; + int predStarNum = result.Item2; - using ImageRegion starRegion = ra1.DeriveCrop(new Rect((int)(ra1.Width * 0.682), (int)(ra1.Width * 0.1823), (int)(ra1.Width * 0.105), (int)(ra1.Width * 0.02345))); - int itemStarNum = GetGridIconsTask.GetStars(starRegion.SrcMat); + // 用CV方法得到的结果 + using var ra1 = CaptureToRectArea(); + using ImageRegion nameRegion = ra1.DeriveCrop(new Rect((int)(ra1.Width * 0.682), (int)(ra1.Width * 0.0625), (int)(ra1.Width * 0.256), (int)(ra1.Width * 0.03125))); + var ocrResult = OcrFactory.Paddle.OcrResult(nameRegion.SrcMat); + string itemName = ocrResult.Text; - // 统计结果 - total_count++; - if (predName == null) - { - logger.LogInformation($"模型没有识别,应为:{itemName}|{itemStarNum}星,❌,正确率{total_acc / total_count:0.00}"); - } - else if (itemName.Contains(predName) && predStarNum == itemStarNum) - { - total_acc++; - logger.LogInformation($"{predName}|{predStarNum}星,✔,正确率{total_acc / total_count:0.00}"); - } - else - { - logger.LogInformation($"{predName}|{predStarNum}星,应为:{itemName}|{itemStarNum}星,❌,正确率{total_acc / total_count:0.00}"); - } - - count--; - if (count <= 0) - { - logger.LogInformation("检查次数已耗尽"); - break; + using ImageRegion starRegion = ra1.DeriveCrop(new Rect((int)(ra1.Width * 0.682), (int)(ra1.Width * 0.1823), (int)(ra1.Width * 0.105), (int)(ra1.Width * 0.02345))); + int itemStarNum = GetGridIconsTask.GetStars(starRegion.SrcMat); + + // 统计结果 + total_count++; + if (predName == null) + { + logger.LogInformation($"模型没有识别,应为:{itemName}|{itemStarNum}星,❌,正确率{total_acc / total_count:0.00}"); + } + else if (itemName.Contains(predName) && predStarNum == itemStarNum) + { + total_acc++; + logger.LogInformation($"{predName}|{predStarNum}星,✔,正确率{total_acc / total_count:0.00}"); + } + else + { + logger.LogInformation($"{predName}|{predStarNum}星,应为:{itemName}|{itemStarNum}星,❌,正确率{total_acc / total_count:0.00}"); + } + + count--; + if (count <= 0) + { + logger.LogInformation("检查次数已耗尽"); + break; + } } } + finally + { + VisionContext.Instance().DrawContent.ClearAll(); + } } /// diff --git a/BetterGenshinImpact/GameTask/Model/Area/DesktopRegion.cs b/BetterGenshinImpact/GameTask/Model/Area/DesktopRegion.cs index f16b13b9..9671e922 100644 --- a/BetterGenshinImpact/GameTask/Model/Area/DesktopRegion.cs +++ b/BetterGenshinImpact/GameTask/Model/Area/DesktopRegion.cs @@ -16,6 +16,12 @@ namespace BetterGenshinImpact.GameTask.Model.Area; public class DesktopRegion : Region { private readonly IMouseSimulator mouse; + + public DesktopRegion(int w, int h, IMouseSimulator? iMouse = null) : base(0, 0, w, h) + { + mouse = iMouse ?? Simulation.SendInput.Mouse; + } + public DesktopRegion() : base(0, 0, PrimaryScreen.WorkingArea.Width, PrimaryScreen.WorkingArea.Height) { mouse = Simulation.SendInput.Mouse; diff --git a/BetterGenshinImpact/GameTask/Model/Area/ImageRegion.cs b/BetterGenshinImpact/GameTask/Model/Area/ImageRegion.cs index fe276517..405766ed 100644 --- a/BetterGenshinImpact/GameTask/Model/Area/ImageRegion.cs +++ b/BetterGenshinImpact/GameTask/Model/Area/ImageRegion.cs @@ -124,8 +124,17 @@ public class ImageRegion : Region } else { + if (ro.UseBinaryMatch) + { + roi = new Mat(); + Cv2.Threshold(CacheGreyMat, roi, ro.BinaryThreshold, 255, ThresholdTypes.Binary); + } + else + { + roi = CacheGreyMat; + } + template = ro.TemplateImageGreyMat; - roi = CacheGreyMat; } if (template == null) diff --git a/BetterGenshinImpact/GameTask/Model/BaseIndependentTask.cs b/BetterGenshinImpact/GameTask/Model/BaseIndependentTask.cs index 486a4d47..1165657e 100644 --- a/BetterGenshinImpact/GameTask/Model/BaseIndependentTask.cs +++ b/BetterGenshinImpact/GameTask/Model/BaseIndependentTask.cs @@ -5,7 +5,7 @@ namespace BetterGenshinImpact.GameTask.Model; public class BaseIndependentTask { - protected SystemInfo Info => TaskContext.Instance().SystemInfo; + protected ISystemInfo Info => TaskContext.Instance().SystemInfo; protected Rect CaptureRect => TaskContext.Instance().SystemInfo.ScaleMax1080PCaptureRect; protected double AssetScale => TaskContext.Instance().SystemInfo.AssetScale; } diff --git a/BetterGenshinImpact/GameTask/Model/GameUI/ArtifactSetFilterScreen.cs b/BetterGenshinImpact/GameTask/Model/GameUI/ArtifactSetFilterScreen.cs index 7a664bf0..9945a342 100644 --- a/BetterGenshinImpact/GameTask/Model/GameUI/ArtifactSetFilterScreen.cs +++ b/BetterGenshinImpact/GameTask/Model/GameUI/ArtifactSetFilterScreen.cs @@ -10,15 +10,17 @@ using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; +using TorchSharp.Modules; namespace BetterGenshinImpact.GameTask.Model.GameUI { - public class ArtifactSetFilterScreen : IAsyncEnumerable + public class ArtifactSetFilterScreen : IAsyncEnumerable> { private readonly GridParams @params; private readonly CancellationToken ct; private readonly ILogger logger; private readonly InputSimulator input = Simulation.SendInput; + internal Action? OnBeforeScroll { get; set; } /// /// 对圣遗物套装筛选界面的操作封装类 @@ -35,42 +37,43 @@ namespace BetterGenshinImpact.GameTask.Model.GameUI this.logger = logger; this.@params = @params; } - public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) + public IAsyncEnumerator> GetAsyncEnumerator(CancellationToken cancellationToken = default) { - return new GridEnumerator(@params.Roi, @params.Columns, new GridScroller(@params, logger, input, ct), ct); + return new GridEnumerator(this, @params.Roi, @params.Columns, new GridScroller(@params, logger, input, ct), ct); } - public class GridEnumerator : IAsyncEnumerator + public class GridEnumerator : IAsyncEnumerator> { + private readonly ArtifactSetFilterScreen owner; private readonly Rect roi; private readonly CancellationToken ct; private readonly int columns; private readonly GridScroller gridScroller; + private record Page(ImageRegion PageRegion, Queue ItemRects); + private Page? currentPage; + private Tuple? current; + Tuple IAsyncEnumerator>.Current => current ?? throw new NullReferenceException(); - private Queue imageRegions; - private ImageRegion? current; - ImageRegion IAsyncEnumerator.Current => current ?? throw new NullReferenceException(); - - internal GridEnumerator(Rect roi, int columns, GridScroller gridScroller, CancellationToken ct) + internal GridEnumerator(ArtifactSetFilterScreen owner, Rect roi, int columns, GridScroller gridScroller, CancellationToken ct) { + this.owner = owner; this.roi = roi; this.ct = ct; this.columns = columns; this.gridScroller = gridScroller; - - this.imageRegions = new Queue(); } public async ValueTask MoveNextAsync() { - if (current == null || this.imageRegions.Count < 1) + if (this.currentPage == null || this.currentPage.ItemRects.Count < 1) { - if (current != null) + if (this.currentPage != null) { using var ra4 = TaskControl.CaptureToRectArea(); ra4.MoveTo(this.roi.X + this.roi.Width / 2, this.roi.Y + this.roi.Height / 2); await TaskControl.Delay(300, ct); + owner.OnBeforeScroll?.Invoke(); if (!await this.gridScroller.TryVerticalScollDown(GetGridItems)) { return false; @@ -78,17 +81,34 @@ namespace BetterGenshinImpact.GameTask.Model.GameUI } using ImageRegion ra = TaskControl.CaptureToRectArea(); - using ImageRegion imageRegion = ra.DeriveCrop(this.roi); - IEnumerable gridItems = GetGridItems(imageRegion.SrcMat, this.columns).Select(imageRegion.DeriveCrop); - this.imageRegions = new Queue(gridItems); + ImageRegion imageRegion = ra.DeriveCrop(this.roi); + try + { + IEnumerable gridRects = GetGridItems(imageRegion.SrcMat, this.columns); + + if (!gridRects.Any()) + { + imageRegion.Dispose(); + return false; + } + + this.currentPage?.PageRegion?.Dispose(); + this.currentPage = new Page(imageRegion, new Queue(gridRects)); + } + catch + { + imageRegion?.Dispose(); + throw; + } } - this.current = this.imageRegions.Dequeue(); + this.current = Tuple.Create(this.currentPage.PageRegion, this.currentPage.ItemRects.Dequeue()); return true; } public ValueTask DisposeAsync() { + this.currentPage?.PageRegion?.Dispose(); return ValueTask.CompletedTask; } } @@ -130,129 +150,11 @@ namespace BetterGenshinImpact.GameTask.Model.GameUI IEnumerable boxes = contours.Select(Cv2.BoundingRect); - double avgWidth = boxes.Average(r => r.Width); - double avgHeight = boxes.Average(r => r.Height); + List cells = GridCell.ClusterToCells(boxes, 10).ToList(); - List cells = ClusterToCells(boxes, 10).ToList(); - - double avgColSpacing; - double avgRowSpace; - { - int count = 0; - int sum = 0; - foreach (var row in cells.GroupBy(t => t.RowNum)) - { - for (int i = 0; i < row.Max(r => r.ColNum); i++) - { - var x1 = row.SingleOrDefault(r => r.ColNum == i); - var x2 = row.SingleOrDefault(r => r.ColNum == i + 1); - if (x1 == null || x2 == null) - { - continue; - } - sum += x2.Rect.X - x1.Rect.X - x1.Rect.Width; - count++; - } - } - avgColSpacing = Math.Round(((double)sum) / count, MidpointRounding.AwayFromZero); - } - { - int count = 0; - int sum = 0; - foreach (var col in cells.GroupBy(t => t.ColNum)) - { - for (int i = 0; i < col.Max(r => r.RowNum); i++) - { - var y1 = col.SingleOrDefault(r => r.RowNum == i); - var y2 = col.SingleOrDefault(r => r.RowNum == i + 1); - if (y1 == null || y2 == null) - { - continue; - } - sum += y2.Rect.Y - y1.Rect.Y - y1.Rect.Height; - count++; - } - } - avgRowSpace = Math.Round(((double)sum) / count, MidpointRounding.AwayFromZero); - } - - int avgLeft = (int)Math.Round(cells.Average(c => c.Rect.X - (avgWidth + avgColSpacing) * c.ColNum), MidpointRounding.AwayFromZero); - int avgTop = (int)Math.Round(cells.Average(c => c.Rect.Y - (avgHeight + avgRowSpace) * c.RowNum), MidpointRounding.AwayFromZero); - - // 遍历方阵,补上缺的Cell - for (int i = 0; i < cells.Max(r => r.ColNum) + 1; i++) - { - for (int j = 0; j < cells.Max(r => r.RowNum) + 1; j++) - { - if (cells.Any(c => c.ColNum == i && c.RowNum == j)) - { - continue; - } - int x = (int)Math.Round(avgLeft + (avgWidth + avgColSpacing) * i, MidpointRounding.AwayFromZero); - int y = (int)Math.Round(avgTop + (avgHeight + avgRowSpace) * j, MidpointRounding.AwayFromZero); - int width = (int)Math.Round(avgWidth, MidpointRounding.AwayFromZero); - int height = (int)Math.Round(avgHeight, MidpointRounding.AwayFromZero); - Cell cell = new Cell(new Rect(x, y, width, height)); - cell.ColNum = i; - cell.RowNum = j; - cells.Add(cell); - } - } + GridCell.FillMissingGridCells(ref cells); return cells.OrderBy(c => c.RowNum).ThenBy(c => c.ColNum).Select(c => c.Rect).ToArray(); } - - /// - /// 具有行号列号的单元格 - /// ColNum和RowNum也是0-based的 - /// 不仅方便编程,ClusterColsAndRows方法也需要一个引用类型 - /// - /// - private class Cell(Rect rect) - { - internal Rect Rect = rect; - internal int ColNum; - internal int RowNum; - } - - /// - /// 把检出的矩形聚簇成类似Excel的单元格集合 - /// - /// - /// - /// - private static IEnumerable ClusterToCells(IEnumerable rects, int threshold) - { - var result = rects.Select(r => new Cell(r)); - result = result.ToArray(); // 必需,不然引用会丢掉。。 - - var orderByX = result.OrderBy(t => t.Rect.Left).ToArray(); - int col = 0; - int? lastX = null; - for (int i = 0; i < orderByX.Length; i++) - { - if (lastX != null && orderByX[i].Rect.X - lastX > threshold) - { - col++; - } - orderByX[i].ColNum = col; - lastX = orderByX[i].Rect.X; - } - - var orderByY = result.OrderBy(t => t.Rect.Top).ToArray(); - int row = 0; - int? lastY = null; - for (int i = 0; i < orderByY.Length; i++) - { - if (lastY != null && orderByY[i].Rect.Y - lastY > threshold) - { - row++; - } - orderByY[i].RowNum = row; - lastY = orderByY[i].Rect.Y; - } - - return result; - } } } diff --git a/BetterGenshinImpact/GameTask/Model/GameUI/GridCell.cs b/BetterGenshinImpact/GameTask/Model/GameUI/GridCell.cs new file mode 100644 index 00000000..149e39b9 --- /dev/null +++ b/BetterGenshinImpact/GameTask/Model/GameUI/GridCell.cs @@ -0,0 +1,162 @@ +using OpenCvSharp; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace BetterGenshinImpact.GameTask.Model.GameUI +{ + /// + /// 具有行号列号的单元格 + /// ColNum和RowNum也是0-based的 + /// 不仅方便编程,ClusterToCells方法也需要一个引用类型 + /// + /// + public class GridCell(Rect rect) + { + public Rect Rect = rect; + public int ColNum; + public int RowNum; + /// + /// 表示该单元格并非CV方法识别得到,而是通过算法推测出的 + /// + public bool IsPhantom; + + /// + /// 把检出的矩形聚簇成类似Excel的单元格集合 + /// + /// + /// + /// + public static IEnumerable ClusterToCells(IEnumerable rects, int threshold) + { + return ClusterToCells(rects.Select(r => Tuple.Create(0, r)), threshold).Select(t => t.Item2).ToArray(); + } + + /// + /// 把检出的矩形聚簇成类似Excel的单元格集合 + /// + /// + /// + /// + public static IEnumerable> ClusterToCells(IEnumerable> rects, int threshold) + { + if (!rects.Any()) + { + return []; + } + + var result = rects.Select(r => new Tuple(r.Item1, new GridCell(r.Item2))); + result = result.ToArray(); // 必需,不然引用会丢掉。。 + + var orderByX = result.OrderBy(t => t.Item2.Rect.Left).ToArray(); + int col = 0; + int? lastX = null; + int avgWidth = (int)rects.Average(r => r.Item2.Width); + for (int i = 0; i < orderByX.Length; i++) + { + if (lastX != null && orderByX[i].Item2.Rect.X - lastX > threshold) + { + col += (int)Math.Round((float)(orderByX[i].Item2.Rect.X - lastX.Value) / (avgWidth + threshold)); + } + orderByX[i].Item2.ColNum = col; + lastX = orderByX[i].Item2.Rect.X; + } + + var orderByY = result.OrderBy(t => t.Item2.Rect.Top).ToArray(); + int row = 0; + int? lastY = null; + int avgHeight = (int)rects.Average(r => r.Item2.Height); + for (int i = 0; i < orderByY.Length; i++) + { + if (lastY != null && orderByY[i].Item2.Rect.Y - lastY > threshold) + { + row += (int)Math.Round((float)(orderByY[i].Item2.Rect.Y - lastY.Value) / (avgHeight + threshold)); // 估算隔了多少行 + } + orderByY[i].Item2.RowNum = row; + lastY = orderByY[i].Item2.Rect.Y; + } + + return result; + } + + /// + /// 遍历方阵,补上缺的Cell + /// + /// + public static void FillMissingGridCells(ref List cells) + { + if (cells.Count <= 0) + { + return; + } + + double avgWidth = cells.Average(c => c.Rect.Width); + double avgHeight = cells.Average(c => c.Rect.Height); + double avgColSpacing; + double avgRowSpace; + { + int count = 0; + int sum = 0; + foreach (var row in cells.GroupBy(t => t.RowNum)) + { + for (int i = 0; i < row.Max(r => r.ColNum); i++) + { + var x1 = row.SingleOrDefault(r => r.ColNum == i); + var x2 = row.SingleOrDefault(r => r.ColNum == i + 1); + if (x1 == null || x2 == null) + { + continue; + } + sum += x2.Rect.X - x1.Rect.X - x1.Rect.Width; + count++; + } + } + avgColSpacing = count == 0 ? 0 : Math.Round(((double)sum) / count, MidpointRounding.AwayFromZero); + } + { + int count = 0; + int sum = 0; + foreach (var col in cells.GroupBy(t => t.ColNum)) + { + for (int i = 0; i < col.Max(r => r.RowNum); i++) + { + var y1 = col.SingleOrDefault(r => r.RowNum == i); + var y2 = col.SingleOrDefault(r => r.RowNum == i + 1); + if (y1 == null || y2 == null) + { + continue; + } + sum += y2.Rect.Y - y1.Rect.Y - y1.Rect.Height; + count++; + } + } + avgRowSpace = count == 0 ? 0 : Math.Round(((double)sum) / count, MidpointRounding.AwayFromZero); + } + + int avgLeft = (int)Math.Round(cells.Average(c => c.Rect.X - (avgWidth + avgColSpacing) * c.ColNum), MidpointRounding.AwayFromZero); + int avgTop = (int)Math.Round(cells.Average(c => c.Rect.Y - (avgHeight + avgRowSpace) * c.RowNum), MidpointRounding.AwayFromZero); + + for (int i = 0; i < cells.Max(r => r.ColNum) + 1; i++) + { + for (int j = 0; j < cells.Max(r => r.RowNum) + 1; j++) + { + if (cells.Any(c => c.ColNum == i && c.RowNum == j)) + { + continue; + } + int x = (int)Math.Round(avgLeft + (avgWidth + avgColSpacing) * i, MidpointRounding.AwayFromZero); + int y = (int)Math.Round(avgTop + (avgHeight + avgRowSpace) * j, MidpointRounding.AwayFromZero); + int width = (int)Math.Round(avgWidth, MidpointRounding.AwayFromZero); + int height = (int)Math.Round(avgHeight, MidpointRounding.AwayFromZero); + GridCell cell = new GridCell(new Rect(x, y, width, height)) + { + ColNum = i, + RowNum = j, + IsPhantom = true + }; + cells.Add(cell); + } + } + } + } +} diff --git a/BetterGenshinImpact/GameTask/Model/GameUI/GridScreen.cs b/BetterGenshinImpact/GameTask/Model/GameUI/GridScreen.cs index f09d1456..abd74a22 100644 --- a/BetterGenshinImpact/GameTask/Model/GameUI/GridScreen.cs +++ b/BetterGenshinImpact/GameTask/Model/GameUI/GridScreen.cs @@ -14,12 +14,27 @@ using System.Threading.Tasks; namespace BetterGenshinImpact.GameTask.Model.GameUI { - public class GridScreen : IAsyncEnumerable + public class GridScreen : IAsyncEnumerable> { private readonly GridParams @params; private readonly CancellationToken ct; private readonly ILogger logger; private readonly InputSimulator input = Simulation.SendInput; + internal Action? OnBeforeScroll { get; set; } + internal Action>>>? OnAfterTurnToNewPage { get; set; } + + /// + /// 提供一个默认的绘制页面上所有识别出的项目的行为 + /// + internal static readonly Action>>> DrawItemsAfterTurnToNewPage = data => + { + (ImageRegion page, var items) = data; + foreach ((Rect rect, bool isPhantom) in items) + { + using ImageRegion item = page.DeriveCrop(rect); + item.DrawSelf($"GridItem{item.GetHashCode()}", isPhantom ? System.Drawing.Pens.Yellow : System.Drawing.Pens.Lime); + } + }; /// /// 对Gird类型界面的操作封装类 @@ -34,20 +49,21 @@ namespace BetterGenshinImpact.GameTask.Model.GameUI { this.ct = ct; this.logger = logger; - if (@params.Columns < 4) + if (@params.Columns < 3) { throw new ArgumentOutOfRangeException(nameof(@params.Columns)); } this.@params = @params; } - public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) + public IAsyncEnumerator> GetAsyncEnumerator(CancellationToken cancellationToken = default) { - return new GridEnumerator(@params.Roi, @params.Columns, input, new GridScroller(@params, logger, input, ct), ct); + return new GridEnumerator(this, @params.Roi, @params.Columns, input, new GridScroller(@params, logger, input, ct), ct); } - public class GridEnumerator : IAsyncEnumerator + public class GridEnumerator : IAsyncEnumerator> { + private readonly GridScreen owner; private readonly Rect roi; private readonly CancellationToken ct; private readonly InputSimulator input = Simulation.SendInput; @@ -59,10 +75,10 @@ namespace BetterGenshinImpact.GameTask.Model.GameUI /// /// 供枚举输出的队列 /// 为了防止Grid的页面元素自动回收复用技术导致item高亮干扰,每次滚动后记录靠近下方的一个item,在下次滚动前主动点击该item - private record Page(Queue ImageRegions, Rect? AntiRecycling); + private record Page(ImageRegion PageRegion, Queue ItemRects, Rect? AntiRecycling); private Page? currentPage; - private ImageRegion current; - ImageRegion IAsyncEnumerator.Current => current; + private Tuple? current; + Tuple IAsyncEnumerator>.Current => current ?? throw new NullReferenceException(); /// /// 滚动操作枚举器 @@ -76,8 +92,9 @@ namespace BetterGenshinImpact.GameTask.Model.GameUI /// /// /// - internal GridEnumerator(Rect roi, int columns, InputSimulator input, GridScroller gridScroller, CancellationToken ct) + internal GridEnumerator(GridScreen owner, Rect roi, int columns, InputSimulator input, GridScroller gridScroller, CancellationToken ct) { + this.owner = owner; this.roi = roi; this.ct = ct; this.input = input; @@ -86,86 +103,7 @@ namespace BetterGenshinImpact.GameTask.Model.GameUI } /// - /// 将图标按Y轴高度简单地进行聚簇,避免因微小差异而乱序 - /// 已知每行的图标之间的Y不会差得太多 - /// - /// 传入的Y列表 - /// - /// 外层是各行从上到下,内层是一行从左到右 - public static List> ClusterRows(IEnumerable regions, int threshold) - { - static int getX(T t) - { - if (t is ImageRegion imageRegion) - { - return imageRegion.X; - } - else if (t is Rect rect) - { - return rect.X; - } - else - { - throw new NotSupportedException(); - } - } - static int getY(T t) - { - if (t is ImageRegion imageRegion) - { - return imageRegion.Y; - } - else if (t is Rect rect) - { - return rect.Y; - } - else - { - throw new NotSupportedException(); - } - } - - // 先对Y排序,便于聚簇 - var sortedRegions = regions.OrderBy(getY).ToList(); - - List> clusters = new List>(); - - if (sortedRegions.Count == 0) - return clusters; - - // 初始化第一个聚簇 - List currentCluster = new List { }; - - foreach (T r in sortedRegions) - { - if (currentCluster.Count <= 0) - { - currentCluster.Add(r); - continue; - } - - T lastInCluster = currentCluster.Last(); - - // 如果当前数字与聚簇中最后一个数字的差值小于阈值,则加入当前聚簇 - if (getY(r) - getY(lastInCluster) <= threshold) - { - currentCluster.Add(r); - } - else - { - // 否则,创建一个新的聚簇 - clusters.Add(currentCluster.OrderBy(getX).ToList()); - currentCluster = new List { r }; - } - } - - // 添加最后一个聚簇 - clusters.Add(currentCluster.OrderBy(getX).ToList()); - - return clusters; - } - - /// + /// 纯cv方法获取 /// 返回未经排序的所有GridItem /// /// @@ -215,7 +153,7 @@ namespace BetterGenshinImpact.GameTask.Model.GameUI { return false; } - return Math.Abs((float)r.Width / r.Height - 0.81) < 0.05; // 按形状筛选 + return Math.Abs((float)r.Width / r.Height - 0.81) < 0.03; // 按形状筛选 }).ToArray(); IEnumerable boxes = contours.Select(Cv2.BoundingRect); @@ -261,6 +199,13 @@ namespace BetterGenshinImpact.GameTask.Model.GameUI return contours; } + /* + * hutaofisher给的划线算法参数,对网格划分效果似乎较好,待应用 + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + canny = cv2.Canny(gray, 25, 50) + hough = cv2.HoughLinesP(canny, 1, np.pi / 180, threshold=500, minLineLength=200, maxLineGap=400) + */ + /// /// 背包界面的背景是把打开界面之前的画面进行了模糊+黑白渐变滤镜+左上角水印叠加处理 /// 放任五彩斑斓的输入,并且允许点击高亮的话处理起来就复杂了 @@ -373,84 +318,149 @@ namespace BetterGenshinImpact.GameTask.Model.GameUI return contours; } + /// + /// 把Rects结果聚簇成Cells,并进行优化 + /// + /// + /// + /// + /// + public static IEnumerable PostProcess(Mat mat, IEnumerable rects, int threshold) + { + if (!rects.Any()) + { + return []; + } + // 根据聚簇结果补漏…… + List cells = GridCell.ClusterToCells(rects, threshold).ToList(); + GridCell.FillMissingGridCells(ref cells); + + // 在末尾处有可能补多了,把底部颜色不符的丢掉…… // PS:群友有直接用底部颜色进行识别的,效果不错 + var result = cells.ToList(); + foreach (var cell in cells.Where(c => c.IsPhantom)) + { + using Mat cellMat = mat.SubMat(cell.Rect); + using Mat bottom = cellMat.GetGridBottom(); + if (!IsCorrectBottomColor(bottom)) + { + result.Remove(cell); + } + } + + return result; + } + public async ValueTask MoveNextAsync() { - if (this.currentPage == null || this.currentPage.ImageRegions.Count < 1) + if (this.currentPage == null || this.currentPage.ItemRects.Count < 1) { - IEnumerable gridItems; - if (this.currentPage != null) // 当前页遍历完了就向下滚动 + ImageRegion? imageRegion = null; + try { - if (this.currentPage.AntiRecycling.HasValue) + if (this.currentPage != null) // 当前页遍历完了就向下滚动 { + if (this.currentPage.AntiRecycling.HasValue) + { + using DesktopRegion desktop = new DesktopRegion(this.input.Mouse); + var (x, y, w, h) = (this.currentPage.AntiRecycling.Value.X, this.currentPage.AntiRecycling.Value.Y, this.currentPage.AntiRecycling.Value.Width, this.currentPage.AntiRecycling.Value.Height); + var (gcX, gcY) = (TaskContext.Instance().SystemInfo.CaptureAreaRect.X, TaskContext.Instance().SystemInfo.CaptureAreaRect.Y); + desktop.ClickTo(gcX + this.roi.X + x + (w / 2d), gcY + this.roi.Y + y + (h / 2d)); + await TaskControl.Delay(500, ct); + desktop.ClickTo(gcX + this.roi.X + x + (w / 2d), gcY + this.roi.Y + y + (h / 2d)); + await TaskControl.Delay(500, ct); + } + + using var ra4 = TaskControl.CaptureToRectArea(); + ra4.MoveTo(this.roi.X + this.roi.Width / 2, this.roi.Y + this.roi.Height / 2); + await TaskControl.Delay(300, ct); + + owner.OnBeforeScroll?.Invoke(); + if (!await this.gridScroller.TryVerticalScollDown((src, columns) => GetGridItems(src, columns))) + { + return false; + } + + using ImageRegion ra = TaskControl.CaptureToRectArea(); + imageRegion = ra.DeriveCrop(this.roi); + } + else + { + // 第一页采集时,主动操作来避免图标高亮 + Rect rect12 = new Rect(0, 0, (int)(this.roi.Width * 1.5 / this.columns), this.roi.Height); + // 双击第三列,采集第一、二列 using DesktopRegion desktop = new DesktopRegion(this.input.Mouse); - var (x, y, w, h) = (this.currentPage.AntiRecycling.Value.X, this.currentPage.AntiRecycling.Value.Y, this.currentPage.AntiRecycling.Value.Width, this.currentPage.AntiRecycling.Value.Height); var (gcX, gcY) = (TaskContext.Instance().SystemInfo.CaptureAreaRect.X, TaskContext.Instance().SystemInfo.CaptureAreaRect.Y); - desktop.ClickTo(gcX + this.roi.X + x + (w / 2d), gcY + this.roi.Y + y + (h / 2d)); + desktop.ClickTo(gcX + this.roi.X + this.roi.Width * 2.5 / this.columns, gcY + this.roi.Y + this.roi.Width * 0.5 / this.columns); + await TaskControl.Delay(300, ct); + desktop.ClickTo(gcX + this.roi.X + this.roi.Width * 2.5 / this.columns, gcY + this.roi.Y + this.roi.Width * 0.5 / this.columns); await TaskControl.Delay(500, ct); - desktop.ClickTo(gcX + this.roi.X + x + (w / 2d), gcY + this.roi.Y + y + (h / 2d)); + + using ImageRegion ra12 = TaskControl.CaptureToRectArea(); + using ImageRegion imageRegion12 = ra12.DeriveCrop(this.roi); + using Mat columns12 = new Mat(imageRegion12.SrcMat, rect12); + + // 双击第一列,采集第二列以后的列 + desktop.ClickTo(gcX + this.roi.X + this.roi.Width * 0.5 / this.columns, gcY + this.roi.Y + this.roi.Width * 0.5 / this.columns); + await TaskControl.Delay(300, ct); + desktop.ClickTo(gcX + this.roi.X + this.roi.Width * 0.5 / this.columns, gcY + this.roi.Y + this.roi.Width * 0.5 / this.columns); await TaskControl.Delay(500, ct); + + using ImageRegion raRest = TaskControl.CaptureToRectArea(); + imageRegion = raRest.DeriveCrop(this.roi); + using Mat subMat12 = imageRegion.SrcMat.SubMat(rect12); + columns12.CopyTo(subMat12); // 拼接两次的采集 } - //BetterGenshinImpact.View.Drawable.VisionContext.Instance().DrawContent.ClearAll(); + var rects = GetGridItems(imageRegion.SrcMat, this.columns); + var cells = PostProcess(imageRegion.SrcMat, rects, (int)(0.025 * this.roi.Height)); - using var ra4 = TaskControl.CaptureToRectArea(); - ra4.MoveTo(this.roi.X + this.roi.Width / 2, this.roi.Y + this.roi.Height / 2); - await TaskControl.Delay(300, ct); - - if (!await this.gridScroller.TryVerticalScollDown((src, columns) => GetGridItems(src, columns))) + if (!cells.Any()) { + imageRegion.Dispose(); return false; } - using ImageRegion ra = TaskControl.CaptureToRectArea(); - using ImageRegion imageRegion = ra.DeriveCrop(this.roi); - gridItems = GetGridItems(imageRegion.SrcMat, this.columns).Select(imageRegion.DeriveCrop); + this.currentPage?.PageRegion?.Dispose(); + this.currentPage = new Page(imageRegion, new Queue(cells.OrderBy(c => c.RowNum).ThenBy(c => c.ColNum).Select(c => c.Rect)), + cells.GroupBy(c => c.RowNum).OrderByDescending(g => g.Key).Skip(1)?.FirstOrDefault()?.OrderBy(c => c.ColNum)?.FirstOrDefault()?.Rect); + + owner.OnAfterTurnToNewPage?.Invoke(Tuple.Create(imageRegion, cells.Select(c => Tuple.Create(c.Rect, c.IsPhantom)))); } - else + catch { - // 第一页采集时,主动操作来避免图标高亮 - // 双击第四列,采集第一、二列 - using DesktopRegion desktop = new DesktopRegion(this.input.Mouse); - var (gcX, gcY) = (TaskContext.Instance().SystemInfo.CaptureAreaRect.X, TaskContext.Instance().SystemInfo.CaptureAreaRect.Y); - desktop.ClickTo(gcX + this.roi.X + this.roi.Width * 3.5 / this.columns, gcY + this.roi.Y + this.roi.Width * 0.5 / this.columns); - await TaskControl.Delay(500, ct); - desktop.ClickTo(gcX + this.roi.X + this.roi.Width * 3.5 / this.columns, gcY + this.roi.Y + this.roi.Width * 0.5 / this.columns); - await TaskControl.Delay(500, ct); - - using ImageRegion ra12 = TaskControl.CaptureToRectArea(); - using ImageRegion imageRegion12 = ra12.DeriveCrop(this.roi); - using Mat columns12 = new Mat(imageRegion12.SrcMat, new Rect(0, 0, (int)(this.roi.Width * 2.5 / this.columns), this.roi.Height)); - IEnumerable columns12Items = GetGridItems(columns12, 2); - // 双击第一列,采集第三列以后的列 - desktop.ClickTo(gcX + this.roi.X + this.roi.Width * 0.5 / this.columns, gcY + this.roi.Y + this.roi.Width * 0.5 / this.columns); - await TaskControl.Delay(500, ct); - desktop.ClickTo(gcX + this.roi.X + this.roi.Width * 0.5 / this.columns, gcY + this.roi.Y + this.roi.Width * 0.5 / this.columns); - await TaskControl.Delay(500, ct); - - using ImageRegion raRest = TaskControl.CaptureToRectArea(); - using ImageRegion imageRegionRest = raRest.DeriveCrop(this.roi); - int restStartX = (int)(this.roi.Width * 1.5 / this.columns); - using Mat columnsRest = new Mat(imageRegionRest.SrcMat, new Rect(restStartX, 0, this.roi.Width - restStartX, this.roi.Height)); - IEnumerable columnsRestItems = GetGridItems(columnsRest, this.columns - 2).Select(r => new Rect(r.X + restStartX, r.Y, r.Width, r.Height)); - - gridItems = columns12Items.Select(imageRegion12.DeriveCrop).Union(columnsRestItems.Select(imageRegionRest.DeriveCrop)).ToArray(); + imageRegion?.Dispose(); + throw; } - - List> clusterRows = ClusterRows(gridItems, (int)(0.025 * this.roi.Height)); - this.currentPage = new Page(new Queue(clusterRows.SelectMany(r => r)), clusterRows.Reverse>().Skip(1)?.FirstOrDefault()?.FirstOrDefault()?.ToRect()); - - //foreach (Rect item in gridItems.Select(r => r.ToRect())) - //{ - // imageRegion.DrawRect(item, item.GetHashCode().ToString(), new System.Drawing.Pen(System.Drawing.Color.Lime)); - //} } - this.current = this.currentPage.ImageRegions.Dequeue(); + this.current = Tuple.Create(this.currentPage.PageRegion, this.currentPage.ItemRects.Dequeue()); return true; } + /// + /// 使用均值比较颜色 + /// + public static bool IsCorrectBottomColor(Mat image, int tolerance = 30) + { + if (image.Empty()) + throw new ArgumentException("输入图像为空"); + + Scalar bgrColor = new Scalar(0xdc, 0xe5, 0xe9); + + // 计算区域的平均颜色 + Scalar meanColor = Cv2.Mean(image); + + // 计算平均颜色与目标颜色的差异 + double diff = Math.Abs(meanColor.Val0 - bgrColor.Val0) + + Math.Abs(meanColor.Val1 - bgrColor.Val1) + + Math.Abs(meanColor.Val2 - bgrColor.Val2); + + return diff <= tolerance * 3; + } + public ValueTask DisposeAsync() { + this.currentPage?.PageRegion?.Dispose(); return ValueTask.CompletedTask; } } diff --git a/BetterGenshinImpact/GameTask/Model/GameUI/GridScreenExtensions.cs b/BetterGenshinImpact/GameTask/Model/GameUI/GridScreenExtensions.cs index e2a1c32b..a0991d98 100644 --- a/BetterGenshinImpact/GameTask/Model/GameUI/GridScreenExtensions.cs +++ b/BetterGenshinImpact/GameTask/Model/GameUI/GridScreenExtensions.cs @@ -15,7 +15,7 @@ namespace BetterGenshinImpact.GameTask.Model.GameUI /// public static string GetGridItemIconText(this Mat mat, IOcrService ocrService) { - Mat subMat = mat.SubMat(mat.Height * 128 / 153, mat.Height * 150 / 153, mat.Width * 5 / 125, mat.Width * 120 / 125); + using Mat subMat = mat.SubMat(mat.Height * 128 / 153, mat.Height * 150 / 153, mat.Width * 5 / 125, mat.Width * 120 / 125); using Mat resize = subMat.Resize(new Size(subMat.Width * 2, subMat.Height * 2)); return ocrService.Ocr(resize); } @@ -28,7 +28,18 @@ namespace BetterGenshinImpact.GameTask.Model.GameUI public static Mat GetGridIcon(this Mat mat) { using Mat resized = mat.Resize(new Size(125, 153)); - return resized.SubMat(0, 125, 0, 125).Clone(); + return resized.SubMat(0, 125, 0, 125); + } + + /// + /// 截取Grid图标中底部的部分 + /// + /// + /// + public static Mat GetGridBottom(this Mat mat) + { + using Mat resized = mat.Resize(new Size(125, 153)); + return resized.SubMat(126, 153, 0, 125); } } } diff --git a/BetterGenshinImpact/GameTask/QuickTeleport/Assets/1920x1080/MapSettingsButton.png b/BetterGenshinImpact/GameTask/QuickTeleport/Assets/1920x1080/MapSettingsButton.png new file mode 100644 index 00000000..5c438f59 Binary files /dev/null and b/BetterGenshinImpact/GameTask/QuickTeleport/Assets/1920x1080/MapSettingsButton.png differ diff --git a/BetterGenshinImpact/GameTask/QuickTeleport/Assets/QuickTeleportAssets.cs b/BetterGenshinImpact/GameTask/QuickTeleport/Assets/QuickTeleportAssets.cs index fad4fdeb..b24e1184 100644 --- a/BetterGenshinImpact/GameTask/QuickTeleport/Assets/QuickTeleportAssets.cs +++ b/BetterGenshinImpact/GameTask/QuickTeleport/Assets/QuickTeleportAssets.cs @@ -14,6 +14,7 @@ public class QuickTeleportAssets : BaseAssets public RecognitionObject TeleportButtonRo; public RecognitionObject MapScaleButtonRo; public RecognitionObject MapCloseButtonRo; + public RecognitionObject MapSettingsButtonRo; public RecognitionObject MapChooseRo; public RecognitionObject MapUndergroundSwitchButtonRo; @@ -89,6 +90,18 @@ public class QuickTeleportAssets : BaseAssets DrawOnWindow = false }.InitTemplate(); + MapSettingsButtonRo = new RecognitionObject + { + Name = "MapSettingsButton", + RecognitionType = RecognitionTypes.TemplateMatch, + TemplateImageMat = GameTaskManager.LoadAssetImage("QuickTeleport", "MapSettingsButton.png"), + RegionOfInterest = new Rect((int)(25 * AssetScale), + (int)(990 * AssetScale), + (int)(58 * AssetScale), + (int)(62 * AssetScale)), + DrawOnWindow = false + }.InitTemplate(); + MapChooseRo = new RecognitionObject { Name = "MapChoose", diff --git a/BetterGenshinImpact/GameTask/SystemControl.cs b/BetterGenshinImpact/GameTask/SystemControl.cs index cd5a26f6..116cdc1e 100644 --- a/BetterGenshinImpact/GameTask/SystemControl.cs +++ b/BetterGenshinImpact/GameTask/SystemControl.cs @@ -1,4 +1,5 @@ -using System; +using BetterGenshinImpact.View.Windows; +using System; using System.Diagnostics; using System.IO; using System.Linq; @@ -19,9 +20,10 @@ public class SystemControl { if (!File.Exists(path)) { - throw new Exception($"原神启动路径 {path} 不存在,请前往 启动——同时启动原神——原神安装路径 重新进行配置!"); + await ThemedMessageBox.ErrorAsync($"原神启动路径 {path} 不存在,请前往 启动——同时启动原神——原神安装路径 重新进行配置!"); + return IntPtr.Zero; } - + // 直接exe启动 Process.Start(new ProcessStartInfo(path) { diff --git a/BetterGenshinImpact/GameTask/TaskContext.cs b/BetterGenshinImpact/GameTask/TaskContext.cs index 7f226c47..ddd16d5a 100644 --- a/BetterGenshinImpact/GameTask/TaskContext.cs +++ b/BetterGenshinImpact/GameTask/TaskContext.cs @@ -1,4 +1,4 @@ -using BetterGenshinImpact.Core.Config; +using BetterGenshinImpact.Core.Config; using BetterGenshinImpact.Core.Simulator; using BetterGenshinImpact.GameTask.Model; using BetterGenshinImpact.Genshin.Settings; @@ -52,7 +52,7 @@ namespace BetterGenshinImpact.GameTask public float DpiScale { get; set; } - public SystemInfo SystemInfo { get; set; } + public ISystemInfo SystemInfo { get; set; } public AllConfig Config { diff --git a/BetterGenshinImpact/GameTask/TaskTriggerDispatcher.cs b/BetterGenshinImpact/GameTask/TaskTriggerDispatcher.cs index a451643d..75e2989b 100644 --- a/BetterGenshinImpact/GameTask/TaskTriggerDispatcher.cs +++ b/BetterGenshinImpact/GameTask/TaskTriggerDispatcher.cs @@ -262,6 +262,10 @@ namespace BetterGenshinImpact.GameTask if (maskWindow.IsExist()) { maskWindow.Show(); + if (!_prevGameActive) + { + maskWindow.BringToTop(); + } } }); // } diff --git a/BetterGenshinImpact/GameTask/UseRedeemCode/GamePreviewLiveDateCalculator.cs b/BetterGenshinImpact/GameTask/UseRedeemCode/GamePreviewLiveDateCalculator.cs new file mode 100644 index 00000000..c56c8fa5 --- /dev/null +++ b/BetterGenshinImpact/GameTask/UseRedeemCode/GamePreviewLiveDateCalculator.cs @@ -0,0 +1,51 @@ +using System; + +namespace BetterGenshinImpact.GameTask.UseRedeemCode; + +public class GamePreviewLiveDateCalculator +{ + private static readonly DateTime StartDate = new DateTime(2025, 10, 10); + private const int IntervalDays = 42; + private const double ValidDays = 3.5; + + /// + /// 计算当前日期是否是前瞻日期 + /// + /// 如果是前瞻日期,返回 true;否则返回 false。 + public static bool IsPreviewDate(DateTime date) + { + TimeSpan difference = date - StartDate; + return difference.Days >= 0 && difference.Days % IntervalDays == 0; + } + + public static void TestIsPreviewDate() + { + IsPreviewDate(new DateTime(2025, 11, 21)); + } + + public bool TestTodayIsPreviewDate() + { + return IsPreviewDate(DateTime.Today); + } + + /// + /// 计算当前时间是否在从前瞻日期开始的2.5天范围内。 + /// + /// 如果在范围内,返回 true;否则返回 false。 + public static bool IsWithinPreviewRange(DateTime now) + { + TimeSpan difference = now.Date - StartDate; + int daysSinceStart = difference.Days; + + if (daysSinceStart < 0) + { + return false; + } + + int intervalCount = daysSinceStart / IntervalDays; + DateTime lastPreviewDate = StartDate.AddDays(intervalCount * IntervalDays); + TimeSpan timeSinceLastPreview = now - lastPreviewDate; + + return timeSinceLastPreview.TotalDays >= 0 && timeSinceLastPreview.TotalDays <= ValidDays; + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/UseRedeemCode/RedeemCodeManager.cs b/BetterGenshinImpact/GameTask/UseRedeemCode/RedeemCodeManager.cs index 131f6cf5..31f77b66 100644 --- a/BetterGenshinImpact/GameTask/UseRedeemCode/RedeemCodeManager.cs +++ b/BetterGenshinImpact/GameTask/UseRedeemCode/RedeemCodeManager.cs @@ -13,6 +13,12 @@ namespace BetterGenshinImpact.GameTask.UseRedeemCode; public class RedeemCodeManager { public static HashSet CancelClipboardHash { get; } = []; + + public static void AddNotDetectClipboardText(string clipboardText) + { + var md5Hash = MD5Helper.ComputeMD5(clipboardText); + CancelClipboardHash.Add(md5Hash); + } public static async Task ImportFromClipboard(string clipboardText) { diff --git a/BetterGenshinImpact/Helpers/MarkdownToFlowDocumentConverter.cs b/BetterGenshinImpact/Helpers/MarkdownToFlowDocumentConverter.cs index 45c7cca6..11a68f46 100644 --- a/BetterGenshinImpact/Helpers/MarkdownToFlowDocumentConverter.cs +++ b/BetterGenshinImpact/Helpers/MarkdownToFlowDocumentConverter.cs @@ -9,6 +9,7 @@ using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Media; using System.Windows.Media.Imaging; +using BetterGenshinImpact.View.Windows; namespace BetterGenshinImpact.Helpers; @@ -1168,7 +1169,7 @@ public static class MarkdownToFlowDocumentConverter try { Clipboard.SetText(RestoreEscapeCharacters(codeText)); - MessageBox.Show("代码已复制到剪贴板!", "复制成功", MessageBoxButton.OK, MessageBoxImage.Information); + ThemedMessageBox.Information("代码已复制到剪贴板!", "复制成功"); } catch (Exception ex) { diff --git a/BetterGenshinImpact/Helpers/RuntimeHelper.cs b/BetterGenshinImpact/Helpers/RuntimeHelper.cs index 691f0263..d08462a7 100644 --- a/BetterGenshinImpact/Helpers/RuntimeHelper.cs +++ b/BetterGenshinImpact/Helpers/RuntimeHelper.cs @@ -1,5 +1,7 @@ using BetterGenshinImpact.Core.Config; using BetterGenshinImpact.GameTask; +using BetterGenshinImpact.Service; +using BetterGenshinImpact.View.Windows; using Microsoft.Extensions.Hosting; using System; using System.ComponentModel; @@ -87,7 +89,19 @@ internal static class RuntimeHelper catch (Exception ex) { Debug.WriteLine(ex); - MessageBox.Error("以管理员权限启动 BetterGI 失败,非管理员权限下所有模拟操作功能均不可用!\r\n请尝试 右键 —— 以管理员身份运行 的方式启动 BetterGI"); + // 延迟显示错误对话框,等待 Config 初始化完成 + Application.Current?.Dispatcher.InvokeAsync(async () => + { + // 轮询等待 Config 初始化完成(最多等待 3 秒) + var timeout = TimeSpan.FromSeconds(3); + var startTime = DateTime.Now; + while (ConfigService.Config == null && DateTime.Now - startTime < timeout) + { + await Task.Delay(50); + } + + ThemedMessageBox.Error("以管理员权限启动 BetterGI 失败,非管理员权限下所有模拟操作功能均不可用!\r\n请尝试 右键 —— 以管理员身份运行 的方式启动 BetterGI"); + }, System.Windows.Threading.DispatcherPriority.ApplicationIdle); return; } } @@ -142,7 +156,7 @@ internal static class RuntimeHelper ? "请不要把主程序exe文件剪切到桌面。正确的做法:请右键点击主程序,在弹出的菜单中选择“发送到”选项,然后选择“桌面创建快捷方式”。" : "请重新安装软件"); - MessageBox.Warning(stringBuilder.ToString()); + ThemedMessageBox.Warning(stringBuilder.ToString()); Environment.Exit(0xFFFF); } } diff --git a/BetterGenshinImpact/Helpers/SecurityControlHelper.cs b/BetterGenshinImpact/Helpers/SecurityControlHelper.cs index b7a84a59..14221052 100644 --- a/BetterGenshinImpact/Helpers/SecurityControlHelper.cs +++ b/BetterGenshinImpact/Helpers/SecurityControlHelper.cs @@ -2,6 +2,7 @@ using System.IO; using System.Security.AccessControl; using BetterGenshinImpact.GameTask.Common; +using BetterGenshinImpact.View.Windows; using Microsoft.Extensions.Logging; namespace BetterGenshinImpact.Helpers; @@ -30,7 +31,7 @@ public static class SecurityControlHelper catch (Exception e) { TaskControl.Logger.LogError("首次运行自动初始化按键绑定异常:" + e.Source + "\r\n--" + Environment.NewLine + e.StackTrace + "\r\n---" + Environment.NewLine + e.Message); - MessageBox.Show("检测到当前 BetterGI 位于C盘,尝试修改目录权限失败,可能会导致WebView2相关的功能无法使用!" + e.Message); + ThemedMessageBox.Warning("检测到当前 BetterGI 位于C盘,尝试修改目录权限失败,可能会导致WebView2相关的功能无法使用!" + e.Message); } } } diff --git a/BetterGenshinImpact/Service/Notifier/EmailNotifier.cs b/BetterGenshinImpact/Service/Notifier/EmailNotifier.cs index f1f76f25..b2bf3b77 100644 --- a/BetterGenshinImpact/Service/Notifier/EmailNotifier.cs +++ b/BetterGenshinImpact/Service/Notifier/EmailNotifier.cs @@ -1,11 +1,11 @@ using System.IO; -using System.Net; -using System.Net.Mail; using System.Text; using System.Threading.Tasks; using BetterGenshinImpact.Service.Notification.Model; using BetterGenshinImpact.Service.Notifier.Exception; using BetterGenshinImpact.Service.Notifier.Interface; +using MailKit.Security; +using MimeKit; using SixLabors.ImageSharp; namespace BetterGenshinImpact.Service.Notifier @@ -38,10 +38,6 @@ namespace BetterGenshinImpact.Service.Notifier _fromEmail = fromEmail; _fromName = fromName; ToEmail = toEmail; - - // 忽略SSL证书验证错误 - ServicePointManager.ServerCertificateValidationCallback = - delegate { return true; }; } // 收件人邮箱 @@ -55,81 +51,105 @@ namespace BetterGenshinImpact.Service.Notifier throw new NotifierException("收件人邮箱地址为空"); } - // 创建一个新的SmtpClient实例(不复用) - using (var smtpClient = new SmtpClient()) + // 创建邮件消息 + var message = new MimeMessage(); + message.From.Add(new MailboxAddress(_fromName, _fromEmail)); + message.To.Add(new MailboxAddress("", ToEmail)); + message.Subject = FormatEmailSubject(content); + + var bodyBuilder = new BodyBuilder { - try + HtmlBody = FormatEmailBody(content) + }; + + // 添加图片附件(如果存在) + if (content.Screenshot != null) + { + using var memoryStream = new MemoryStream(); + // 将图片保存到内存流 + await content.Screenshot.SaveAsJpegAsync(memoryStream); + memoryStream.Position = 0; // 重置流位置 + + // 添加附件 + var attachment = await bodyBuilder.Attachments.AddAsync("screenshot.jpg", memoryStream, ContentType.Parse("image/jpeg")); + attachment.ContentId = "screenshot"; + } + + message.Body = bodyBuilder.ToMessageBody(); + + // 使用 MailKit 发送邮件 + using var smtpClient = new MailKit.Net.Smtp.SmtpClient(); + try + { + // 根据服务器和端口选择合适的连接方式 + var secureSocketOptions = GetSecureSocketOptions(); + + await smtpClient.ConnectAsync(_smtpServer, _smtpPort, secureSocketOptions); + + // 如果服务器需要认证,则进行登录 + if (!string.IsNullOrEmpty(_smtpUsername)) { - // 配置SMTP客户端 - smtpClient.Host = _smtpServer; - smtpClient.Port = _smtpPort; - smtpClient.EnableSsl = true; - smtpClient.DeliveryMethod = SmtpDeliveryMethod.Network; - smtpClient.UseDefaultCredentials = false; - smtpClient.Credentials = new NetworkCredential(_smtpUsername, _smtpPassword); - smtpClient.Timeout = 30000; // 30秒超时 - - // 创建邮件 - using (var mailMessage = new MailMessage()) - { - mailMessage.From = new MailAddress(_fromEmail, _fromName); - mailMessage.To.Add(ToEmail); - mailMessage.Subject = FormatEmailSubject(content); - mailMessage.Body = FormatEmailBody(content); - mailMessage.IsBodyHtml = true; - mailMessage.BodyEncoding = Encoding.UTF8; - mailMessage.SubjectEncoding = Encoding.UTF8; - - // 添加图片附件(如果存在) - if (content.Screenshot != null) - { - var tempPath = Path.GetTempFileName() + ".jpg"; - try - { - // 保存图片到临时文件 - await content.Screenshot.SaveAsJpegAsync(tempPath); - - // 从文件添加附件 - var attachment = new Attachment(tempPath); - mailMessage.Attachments.Add(attachment); - - // 发送邮件 - await smtpClient.SendMailAsync(mailMessage); - - // 清理附件和临时文件 - attachment.Dispose(); - if (File.Exists(tempPath)) File.Delete(tempPath); - } - catch (System.Exception ex) - { - // 尝试清理临时文件 - try - { - if (File.Exists(tempPath)) File.Delete(tempPath); - } - catch - { - /* 忽略清理错误 */ - } - - throw new NotifierException($"发送邮件失败: {ex.Message}"); - } - } - else - { - // 没有图片时直接发送 - await smtpClient.SendMailAsync(mailMessage); - } - } - } - catch (System.Exception ex) - { - var errorMessage = $"发送邮件失败: {ex.Message}"; - throw new NotifierException(errorMessage); + await smtpClient.AuthenticateAsync(_smtpUsername, _smtpPassword); } + + await smtpClient.SendAsync(message); + await smtpClient.DisconnectAsync(true); + } + catch (System.Exception ex) + { + var errorMessage = $"发送邮件失败: {ex.Message}"; + throw new NotifierException(errorMessage); } } + /// + /// 根据服务器地址和端口号选择合适的 SecureSocketOptions + /// + private SecureSocketOptions GetSecureSocketOptions() + { + // 对于已知的特殊服务器配置 + if (IsKnownServerWithSpecialRequirements(_smtpServer, _smtpPort, out var secureSocketOption)) + { + return secureSocketOption; + } + + // 通用规则 + return _smtpPort switch + { + 465 => SecureSocketOptions.SslOnConnect, + 587 => SecureSocketOptions.StartTls, // 大多数服务商使用 STARTTLS + 25 => SecureSocketOptions.None, + _ => SecureSocketOptions.Auto // 其他端口让 MailKit 自动协商 + }; + } + + /// + /// 检查是否是已知有特殊要求的服务器 + /// + private bool IsKnownServerWithSpecialRequirements(string server, int port, out SecureSocketOptions option) + { + option = SecureSocketOptions.Auto; + + // 网易邮箱系列 - 587 端口使用 SSL + if ((server.Contains("163.com") || + server.Contains("126.com") || + server.Contains("yeah.net")) && port == 587) + { + option = SecureSocketOptions.SslOnConnect; + return true; + } + + // 可以继续添加其他已知的特殊配置 + // 例如: + // if (server.Contains("some-special-server.com") && port == 587) + // { + // option = SecureSocketOptions.SslOnConnect; + // return true; + // } + + return false; + } + private string FormatEmailSubject(BaseNotificationData content) { // 可以根据实际需求自定义邮件主题格式 @@ -159,9 +179,13 @@ namespace BetterGenshinImpact.Service.Notifier } // 添加提示信息 - builder.AppendLine("

如有截图,请查看附件。

"); + if (content.Screenshot != null) + { + builder.AppendLine("

截图已作为附件添加到邮件中。

"); + } + builder.AppendLine(""); return builder.ToString(); } } -} +} \ No newline at end of file diff --git a/BetterGenshinImpact/Service/ScriptService.cs b/BetterGenshinImpact/Service/ScriptService.cs index 29285ced..7ba75295 100644 --- a/BetterGenshinImpact/Service/ScriptService.cs +++ b/BetterGenshinImpact/Service/ScriptService.cs @@ -486,6 +486,7 @@ public partial class ScriptService : IScriptService target.JsScriptSettingsObject = source.JsScriptSettingsObject; target.GroupInfo = source.GroupInfo; target.AllowJsNotification = source.AllowJsNotification; + target.AllowJsHTTPHash = source.AllowJsHTTPHash; target.SkipFlag = source.SkipFlag; } diff --git a/BetterGenshinImpact/Service/UpdateService.cs b/BetterGenshinImpact/Service/UpdateService.cs index 3823ed36..8c77fa32 100644 --- a/BetterGenshinImpact/Service/UpdateService.cs +++ b/BetterGenshinImpact/Service/UpdateService.cs @@ -73,7 +73,7 @@ public class UpdateService : IUpdateService { if (option.Trigger == UpdateTrigger.Manual) { - await MessageBox.InformationAsync("当前已是最新版本!"); + await ThemedMessageBox.InformationAsync("当前已是最新版本!"); } return; @@ -130,7 +130,7 @@ public class UpdateService : IUpdateService string updaterExePath = Global.Absolute("BetterGI.update.exe"); if (!File.Exists(updaterExePath)) { - await MessageBox.ErrorAsync("更新程序不存在,请选择其他更新方式!"); + await ThemedMessageBox.ErrorAsync("更新程序不存在,请选择其他更新方式!"); return; } diff --git a/BetterGenshinImpact/View/Converters/BooleanToVisibilityRevertConverter.cs b/BetterGenshinImpact/View/Converters/BooleanToVisibilityRevertConverter.cs index 4b90e5bb..f6dd2d45 100644 --- a/BetterGenshinImpact/View/Converters/BooleanToVisibilityRevertConverter.cs +++ b/BetterGenshinImpact/View/Converters/BooleanToVisibilityRevertConverter.cs @@ -9,7 +9,15 @@ public sealed class BooleanToVisibilityRevertConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { - return (value is bool v && v) ? Visibility.Collapsed : Visibility.Visible; + if (value is string str) + { + return string.IsNullOrEmpty(str) ? Visibility.Collapsed : Visibility.Visible; + } + else if (value is bool b) + { + return b ? Visibility.Collapsed : Visibility.Visible; + } + return Visibility.Visible; } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) diff --git a/BetterGenshinImpact/View/Converters/NotNullConverter.cs b/BetterGenshinImpact/View/Converters/NotNullConverter.cs index 1e745d8d..945d721b 100644 --- a/BetterGenshinImpact/View/Converters/NotNullConverter.cs +++ b/BetterGenshinImpact/View/Converters/NotNullConverter.cs @@ -8,6 +8,11 @@ namespace BetterGenshinImpact.View.Converters { public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { + if (value is string str) + { + return !string.IsNullOrWhiteSpace(str); + } + return value != null; } diff --git a/BetterGenshinImpact/View/MainWindow.xaml b/BetterGenshinImpact/View/MainWindow.xaml index feb33d71..a2827c0f 100644 --- a/BetterGenshinImpact/View/MainWindow.xaml +++ b/BetterGenshinImpact/View/MainWindow.xaml @@ -200,13 +200,22 @@ - + - + - - + + + [00:00:00 INF] 更好的原神 @@ -143,10 +146,18 @@ Text="{Binding Name}"> @@ -196,8 +207,11 @@ FontSize="34" FontStretch="Medium" FontWeight="DemiBold" - Foreground="White" - Text="西" /> + Text="西"> + + + + + Text="南"> + + + + + Text="东"> + + + + + Text="北"> + + + + @@ -243,8 +266,11 @@ VerticalAlignment="Center" FontFamily="{StaticResource DigitalThemeFontFamily}" FontSize="16" - Foreground="White" - Text="{Binding Fps}" /> + Text="{Binding Fps}"> + + + + diff --git a/BetterGenshinImpact/View/MaskWindow.xaml.cs b/BetterGenshinImpact/View/MaskWindow.xaml.cs index 2bf7b4eb..6c1dc487 100644 --- a/BetterGenshinImpact/View/MaskWindow.xaml.cs +++ b/BetterGenshinImpact/View/MaskWindow.xaml.cs @@ -68,6 +68,11 @@ public partial class MaskWindow : Window return _maskWindow != null && PresentationSource.FromVisual(_maskWindow) != null; } + public void BringToTop() + { + User32.BringWindowToTop(new WindowInteropHelper(this).Handle); + } + public void RefreshPosition() { if (TaskContext.Instance().Config.MaskWindowConfig.UseSubform) @@ -92,6 +97,7 @@ public partial class MaskWindow : Window Top = currentRect.Top / dpiScale; Width = currentRect.Width / dpiScale; Height = currentRect.Height / dpiScale; + BringToTop(); }); } diff --git a/BetterGenshinImpact/View/Pages/CommonSettingsPage.xaml b/BetterGenshinImpact/View/Pages/CommonSettingsPage.xaml index f79df523..ebf7a441 100644 --- a/BetterGenshinImpact/View/Pages/CommonSettingsPage.xaml +++ b/BetterGenshinImpact/View/Pages/CommonSettingsPage.xaml @@ -320,6 +320,46 @@ + + + + + + + + + + + + + + + - + + \ No newline at end of file diff --git a/BetterGenshinImpact/View/Pages/View/PathingConfigView.xaml b/BetterGenshinImpact/View/Pages/View/PathingConfigView.xaml index 754c9c8a..9edf80eb 100644 --- a/BetterGenshinImpact/View/Pages/View/PathingConfigView.xaml +++ b/BetterGenshinImpact/View/Pages/View/PathingConfigView.xaml @@ -16,6 +16,8 @@ d:DataContext="{d:DesignInstance Type=view:PathingConfigViewModel}" d:DesignHeight="1000" d:DesignWidth="500" + ExtendsContentIntoTitleBar="True" + WindowBackdropType="Auto" mc:Ignorable="d"> @@ -291,7 +293,7 @@ - + diff --git a/BetterGenshinImpact/View/Pages/View/PathingConfigView.xaml.cs b/BetterGenshinImpact/View/Pages/View/PathingConfigView.xaml.cs index 29c42fb0..eba5e6cd 100644 --- a/BetterGenshinImpact/View/Pages/View/PathingConfigView.xaml.cs +++ b/BetterGenshinImpact/View/Pages/View/PathingConfigView.xaml.cs @@ -1,4 +1,5 @@ -using BetterGenshinImpact.ViewModel.Pages.View; +using BetterGenshinImpact.Helpers.Ui; +using BetterGenshinImpact.ViewModel.Pages.View; namespace BetterGenshinImpact.View.Pages.View; @@ -13,5 +14,6 @@ public partial class PathingConfigView { DataContext = ViewModel = viewModel; InitializeComponent(); + SourceInitialized += (s, e) => WindowHelper.TryApplySystemBackdrop(this); } } diff --git a/BetterGenshinImpact/View/Pages/View/ScriptGroupConfigView.xaml b/BetterGenshinImpact/View/Pages/View/ScriptGroupConfigView.xaml index 53d29998..8224a352 100644 --- a/BetterGenshinImpact/View/Pages/View/ScriptGroupConfigView.xaml +++ b/BetterGenshinImpact/View/Pages/View/ScriptGroupConfigView.xaml @@ -13,10 +13,22 @@ ui:Design.Background="{DynamicResource ApplicationBackgroundBrush}" ui:Design.Foreground="{DynamicResource TextFillColorPrimaryBrush}" mc:Ignorable="d"> - + + + + + + + + + + + + + - + + - + + + + + + + + + + + + @@ -1145,13 +1220,14 @@ + - + - - @@ -1266,7 +1352,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/BetterGenshinImpact/View/Windows/FeedWindow.xaml.cs b/BetterGenshinImpact/View/Windows/FeedWindow.xaml.cs new file mode 100644 index 00000000..cf001821 --- /dev/null +++ b/BetterGenshinImpact/View/Windows/FeedWindow.xaml.cs @@ -0,0 +1,41 @@ +using BetterGenshinImpact.Helpers.Ui; +using BetterGenshinImpact.ViewModel.Windows; +using System; +using System.Windows; + +namespace BetterGenshinImpact.View.Windows; + +public partial class FeedWindow +{ + public FeedWindowViewModel ViewModel { get; } + + public FeedWindow(FeedWindowViewModel viewModel) + { + DataContext = ViewModel = viewModel; + InitializeComponent(); + + this.Loaded += FeedWindow_Loaded; + this.SourceInitialized += FeedWindow_SourceInitialized; + } + + public FeedWindow() : this(new FeedWindowViewModel()) + { + } + + private void FeedWindow_SourceInitialized(object? sender, EventArgs e) + { + // 应用与主窗口相同的背景主题 + WindowHelper.TryApplySystemBackdrop(this); + } + + private void FeedWindow_Loaded(object sender, RoutedEventArgs e) + { + // 窗口加载完成后拉取远程兑换码数据 + _ = ViewModel.LoadRemoteDataAsync(); + } + + private void BtnCloseClick(object sender, RoutedEventArgs e) + { + Close(); + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/View/Windows/JsonMonoDialog.xaml.cs b/BetterGenshinImpact/View/Windows/JsonMonoDialog.xaml.cs index e2aff6ed..8e7b2fc1 100644 --- a/BetterGenshinImpact/View/Windows/JsonMonoDialog.xaml.cs +++ b/BetterGenshinImpact/View/Windows/JsonMonoDialog.xaml.cs @@ -1,4 +1,5 @@ -using BetterGenshinImpact.ViewModel.Windows; +using BetterGenshinImpact.Helpers.Ui; +using BetterGenshinImpact.ViewModel.Windows; using System.Windows; using Wpf.Ui.Controls; @@ -12,6 +13,7 @@ public partial class JsonMonoDialog : FluentWindow { DataContext = ViewModel = new(path); InitializeComponent(); + SourceInitialized += (s, e) => WindowHelper.TryApplySystemBackdrop(this); // Manual MVVM binding JsonCodeBox.TextChanged += (_, _) => ViewModel.JsonText = JsonCodeBox.Text; diff --git a/BetterGenshinImpact/View/Windows/KeyBindingsWindow.xaml.cs b/BetterGenshinImpact/View/Windows/KeyBindingsWindow.xaml.cs index 2828f38f..afff08f6 100644 --- a/BetterGenshinImpact/View/Windows/KeyBindingsWindow.xaml.cs +++ b/BetterGenshinImpact/View/Windows/KeyBindingsWindow.xaml.cs @@ -1,3 +1,4 @@ +using BetterGenshinImpact.Helpers.Ui; using BetterGenshinImpact.View.Pages; using Wpf.Ui.Controls; using Grid = System.Windows.Controls.Grid; @@ -33,6 +34,7 @@ public partial class KeyBindingsWindow : FluentWindow public KeyBindingsWindow() { InitializeComponent(); + SourceInitialized += (s, e) => WindowHelper.TryApplySystemBackdrop(this); var page = App.GetService(); Grid.SetRow(page!, 1); diff --git a/BetterGenshinImpact/View/Windows/MapPathingDevWindow.xaml b/BetterGenshinImpact/View/Windows/MapPathingDevWindow.xaml index 121ac816..db11106f 100644 --- a/BetterGenshinImpact/View/Windows/MapPathingDevWindow.xaml +++ b/BetterGenshinImpact/View/Windows/MapPathingDevWindow.xaml @@ -11,6 +11,8 @@ ui:Design.Foreground="{DynamicResource TextFillColorPrimaryBrush}" Width="320" Height="260" + ExtendsContentIntoTitleBar="True" + WindowBackdropType="Auto" mc:Ignorable="d"> diff --git a/BetterGenshinImpact/View/Windows/MapPathingDevWindow.xaml.cs b/BetterGenshinImpact/View/Windows/MapPathingDevWindow.xaml.cs index d414723e..efb8c017 100644 --- a/BetterGenshinImpact/View/Windows/MapPathingDevWindow.xaml.cs +++ b/BetterGenshinImpact/View/Windows/MapPathingDevWindow.xaml.cs @@ -1,4 +1,5 @@ -using MapPathingDevViewModel = BetterGenshinImpact.ViewModel.Windows.MapPathingDevViewModel; +using BetterGenshinImpact.Helpers.Ui; +using MapPathingDevViewModel = BetterGenshinImpact.ViewModel.Windows.MapPathingDevViewModel; namespace BetterGenshinImpact.View.Windows; @@ -10,5 +11,6 @@ public partial class MapPathingDevWindow { DataContext = ViewModel = new MapPathingDevViewModel(); InitializeComponent(); + SourceInitialized += (s, e) => WindowHelper.TryApplySystemBackdrop(this); } } \ No newline at end of file diff --git a/BetterGenshinImpact/View/Windows/MapViewer.xaml.cs b/BetterGenshinImpact/View/Windows/MapViewer.xaml.cs index 5fe17e08..94aaf290 100644 --- a/BetterGenshinImpact/View/Windows/MapViewer.xaml.cs +++ b/BetterGenshinImpact/View/Windows/MapViewer.xaml.cs @@ -1,4 +1,5 @@ -using BetterGenshinImpact.ViewModel.Windows; +using BetterGenshinImpact.Helpers.Ui; +using BetterGenshinImpact.ViewModel.Windows; namespace BetterGenshinImpact.View.Windows; @@ -10,5 +11,6 @@ public partial class MapViewer { DataContext = ViewModel = new MapViewerViewModel(mapName); InitializeComponent(); + SourceInitialized += (s, e) => WindowHelper.TryApplySystemBackdrop(this); } } diff --git a/BetterGenshinImpact/View/Windows/RepoUpdateDialog.xaml b/BetterGenshinImpact/View/Windows/RepoUpdateDialog.xaml new file mode 100644 index 00000000..9875aa80 --- /dev/null +++ b/BetterGenshinImpact/View/Windows/RepoUpdateDialog.xaml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BetterGenshinImpact/View/Windows/RepoUpdateDialog.xaml.cs b/BetterGenshinImpact/View/Windows/RepoUpdateDialog.xaml.cs new file mode 100644 index 00000000..970fbeaa --- /dev/null +++ b/BetterGenshinImpact/View/Windows/RepoUpdateDialog.xaml.cs @@ -0,0 +1,125 @@ +using System; +using System.Threading.Tasks; +using System.Windows; +using BetterGenshinImpact.Helpers.Ui; +using MessageBoxResult = Wpf.Ui.Controls.MessageBoxResult; + +namespace BetterGenshinImpact.View.Windows; + +/// +/// 仓库更新提示对话框 +/// +public partial class RepoUpdateDialog : Wpf.Ui.Controls.FluentWindow +{ + private System.Windows.Threading.DispatcherTimer? _dialogTimer; + private int _remainingSeconds; + private readonly int _daysSinceUpdate; + private TaskCompletionSource? _taskCompletionSource; + + /// + /// 初始化仓库更新提示对话框 + /// + /// 距上次更新的天数 + public RepoUpdateDialog(int daysSinceUpdate) + { + _daysSinceUpdate = daysSinceUpdate; + + InitializeComponent(); + + // 配置窗口属性 + Title = "仓库更新提示"; + MessageTextBlock.Text = $"脚本仓库已经 {daysSinceUpdate} 天未更新\n\n温馨提示:\n脚本内容跟随仓库版本,旧版仓库会订阅到旧版脚本。\n更新仓库后需要重新订阅脚本,以更新脚本内容。\n\n是否立即更新?"; + Owner = Application.Current.MainWindow; + + // 注册事件 + SourceInitialized += OnSourceInitialized; + Loaded += OnLoaded; + Closed += OnClosed; + } + + private void OnSourceInitialized(object? sender, EventArgs e) + { + WindowHelper.TryApplySystemBackdrop(this); + } + + private void OnLoaded(object sender, RoutedEventArgs e) + { + // 计算倒计时秒数:30 天为 5 秒,超过 30 天每多 2 天增加 1 秒 + _remainingSeconds = 5 + (_daysSinceUpdate - 30) / 2; + SecondaryButton.Content = $"直接打开 ({_remainingSeconds}s)"; + StartDialogTimer(); + } + + private void OnClosed(object? sender, EventArgs e) + { + StopDialogTimer(); + _taskCompletionSource?.TrySetResult(MessageBoxResult.None); + } + + /// + /// 显示对话框并等待结果 + /// + public Task ShowDialogAsync() + { + _taskCompletionSource = new TaskCompletionSource(); + ShowDialog(); + return _taskCompletionSource.Task; + } + + private void PrimaryButton_Click(object sender, RoutedEventArgs e) + { + _taskCompletionSource?.TrySetResult(MessageBoxResult.Primary); + Close(); + } + + private void SecondaryButton_Click(object sender, RoutedEventArgs e) + { + _taskCompletionSource?.TrySetResult(MessageBoxResult.Secondary); + Close(); + } + + /// + /// 启动对话框定时器 + /// + private void StartDialogTimer() + { + // 创建定时器 + _dialogTimer = new System.Windows.Threading.DispatcherTimer + { + Interval = TimeSpan.FromSeconds(1) + }; + + _dialogTimer.Tick += OnTimerTick; + _dialogTimer.Start(); + } + + private void OnTimerTick(object? sender, EventArgs e) + { + _remainingSeconds--; + + if (_remainingSeconds > 0) + { + SecondaryButton.Content = $"直接打开 ({_remainingSeconds}s)"; + } + else + { + // 倒计时结束,启用按钮 + SecondaryButton.Content = "直接打开"; + SecondaryButton.IsEnabled = true; + _dialogTimer?.Stop(); + } + } + + /// + /// 停止对话框定时器 + /// + private void StopDialogTimer() + { + if (_dialogTimer != null) + { + _dialogTimer.Tick -= OnTimerTick; + _dialogTimer.Stop(); + _dialogTimer = null; + } + } +} diff --git a/BetterGenshinImpact/View/Windows/ScriptRepoWindow.xaml b/BetterGenshinImpact/View/Windows/ScriptRepoWindow.xaml index b77b3d85..8088a148 100644 --- a/BetterGenshinImpact/View/Windows/ScriptRepoWindow.xaml +++ b/BetterGenshinImpact/View/Windows/ScriptRepoWindow.xaml @@ -62,6 +62,8 @@ + + @@ -81,7 +83,8 @@ VerticalAlignment="Center" DisplayMemberPath="Name" ItemsSource="{Binding RepoChannels}" - SelectedItem="{Binding SelectedRepoChannel}" /> + SelectedItem="{Binding SelectedRepoChannel}" + IsTextSearchEnabled="False" /> @@ -96,16 +99,80 @@ VerticalAlignment="Center" Foreground="{ui:ThemeResource TextFillColorPrimaryBrush}" Text="仓库地址:" /> + + + + + + + + + + Text="{Binding Config.CustomRepoUrl, UpdateSourceTrigger=PropertyChanged}" + Visibility="{Binding IsRepoUrlReadOnly, Converter={StaticResource BooleanToVisibilityRevertConverter}}" /> - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -248,7 +315,8 @@ Command="{Binding OpenLocalScriptRepoCommand}" Content="打开仓库" Icon="{ui:SymbolIcon BookStar24}" - HorizontalAlignment="Center" /> + HorizontalAlignment="Center" + IsEnabled="{Binding IsUpdating, Converter={StaticResource InverseBooleanConverter}}" /> diff --git a/BetterGenshinImpact/View/Windows/ScriptRepoWindow.xaml.cs b/BetterGenshinImpact/View/Windows/ScriptRepoWindow.xaml.cs index 06463119..6c09fd51 100644 --- a/BetterGenshinImpact/View/Windows/ScriptRepoWindow.xaml.cs +++ b/BetterGenshinImpact/View/Windows/ScriptRepoWindow.xaml.cs @@ -3,8 +3,10 @@ using BetterGenshinImpact.Core.Script; using BetterGenshinImpact.GameTask; using BetterGenshinImpact.Helpers; using BetterGenshinImpact.Helpers.Ui; +using BetterGenshinImpact.Helpers.Win32; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using Meziantou.Framework.Win32; using Microsoft.Win32; using System; using System.Collections.ObjectModel; @@ -16,6 +18,8 @@ using System.Linq; using System.Net.Http; using System.Threading.Tasks; using System.Windows; +using System.Windows.Data; +using System.Globalization; using System.Windows.Navigation; using Wpf.Ui.Violeta.Controls; @@ -53,20 +57,47 @@ public partial class ScriptRepoWindow [ObservableProperty] private string _updateProgressText = "准备更新,请耐心等待..."; [ObservableProperty] private ScriptConfig _config = TaskContext.Instance().Config.ScriptConfig; + // Git 凭据相关属性 + private const string GitCredentialAppName = "BetterGenshinImpact.GitCredentials"; + + [ObservableProperty] private string _gitUsername = ""; + [ObservableProperty] private string _gitToken = ""; + // 在线更新相关属性 [ObservableProperty] private string _onlineDownloadUrl = ""; + // 获取当前仓库URL(用于界面显示) + public string CurrentRepoUrl + { + get + { + if (SelectedRepoChannel == null) + { + return ""; + } + return SelectedRepoChannel.Name == "自定义" ? Config.CustomRepoUrl : SelectedRepoChannel.Url; + } + } + public ScriptRepoWindow() { InitializeRepoChannels(); + LoadCredentialsFromManager(); InitializeComponent(); DataContext = this; Config.PropertyChanged += OnConfigPropertyChanged; PropertyChanged += OnPropertyChanged; + + // 设置 PasswordBox 的初始值 + Loaded += (s, e) => GitTokenPasswordBox.Password = GitToken; + SourceInitialized += (s, e) => { // 应用系统背景 WindowHelper.TryApplySystemBackdrop(this); + + // 设置仓库地址的只读状态 + IsRepoUrlReadOnly = SelectedRepoChannel == null || SelectedRepoChannel.Name != "自定义"; }; } @@ -82,6 +113,34 @@ public partial class ScriptRepoWindow { OnIsUpdatingChanged(); } + // 监听 GitUsername 和 GitToken 变化,保存到凭据管理器 + else if (e.PropertyName == nameof(GitUsername) || e.PropertyName == nameof(GitToken)) + { + SaveCredentialsToManager(); + } + } + + /// + /// 从 Windows 凭据管理器加载 Git 凭据 + /// + private void LoadCredentialsFromManager() + { + var credential = CredentialManagerHelper.ReadCredential(GitCredentialAppName); + GitUsername = credential?.UserName ?? ""; + GitToken = credential?.Password ?? ""; + } + + /// + /// 保存 Git 凭据到 Windows 凭据管理器 + /// + private void SaveCredentialsToManager() + { + CredentialManagerHelper.SaveCredential( + GitCredentialAppName, + GitUsername, + GitToken, + "Git credentials for BetterGenshinImpact script repository", + CredentialPersistence.LocalMachine); } ~ScriptRepoWindow() @@ -92,9 +151,10 @@ public partial class ScriptRepoWindow private void OnConfigPropertyChanged(object? sender, PropertyChangedEventArgs e) { - if (e.PropertyName == nameof(ScriptConfig.SelectedRepoUrl)) + // 监听CustomRepoUrl变化,通知界面更新显示 + if (e.PropertyName == nameof(ScriptConfig.CustomRepoUrl)) { - OnConfigSelectedRepoUrlChanged(); + OnPropertyChanged(nameof(CurrentRepoUrl)); } } @@ -114,33 +174,28 @@ public partial class ScriptRepoWindow { new("CNB", "https://cnb.cool/bettergi/bettergi-scripts-list"), new("GitCode", "https://gitcode.com/huiyadanli/bettergi-scripts-list"), - // 暂时无法使用 - // new("Gitee", "https://gitee.com/babalae/bettergi-scripts-list"), new("GitHub", "https://github.com/babalae/bettergi-scripts-list"), new("自定义", "https://example.com/custom-repo") }; - if (string.IsNullOrEmpty(Config.SelectedRepoUrl)) + // 根据配置中保存的渠道名称恢复选择 + if (string.IsNullOrEmpty(Config.SelectedChannelName)) { // 默认选中第一个渠道 SelectedRepoChannel = _repoChannels[0]; - Config.SelectedRepoUrl = SelectedRepoChannel.Url; + Config.SelectedChannelName = SelectedRepoChannel.Name; } else { - // 尝试根据配置中的URL找到对应的渠道 - OnConfigSelectedRepoUrlChanged(); - } - } + // 根据保存的渠道名称找到对应的渠道 + var savedChannel = _repoChannels.FirstOrDefault(c => c.Name == Config.SelectedChannelName); + SelectedRepoChannel = savedChannel ?? _repoChannels[0]; - // Config.SelectedRepoUrl 变化 - private void OnConfigSelectedRepoUrlChanged() - { - // 如果配置中的URL与当前选中渠道不一致,更新选中渠道 - if (string.IsNullOrEmpty(SelectedRepoChannel?.Url) || SelectedRepoChannel.Url != Config.SelectedRepoUrl) - { - SelectedRepoChannel = _repoChannels.FirstOrDefault(c => c.Url == Config.SelectedRepoUrl) ?? - _repoChannels.FirstOrDefault(c => c.Name == "自定义") ?? _repoChannels[0]; + // 如果找不到保存的渠道,更新配置为默认渠道 + if (savedChannel == null) + { + Config.SelectedChannelName = _repoChannels[0].Name; + } } } @@ -151,15 +206,14 @@ public partial class ScriptRepoWindow return; } + // 保存选择的渠道名称 + Config.SelectedChannelName = SelectedRepoChannel.Name; + // 更新仓库地址只读状态 IsRepoUrlReadOnly = SelectedRepoChannel.Name != "自定义"; - // 更新配置中的选中仓库URL - if (SelectedRepoChannel.Name != "自定义") - { - // 如果不是自定义渠道,直接使用选中渠道的URL - Config.SelectedRepoUrl = SelectedRepoChannel.Url; - } + // 通知界面更新CurrentRepoUrl + OnPropertyChanged(nameof(CurrentRepoUrl)); } [RelayCommand] @@ -170,11 +224,31 @@ public partial class ScriptRepoWindow Toast.Warning("请选择一个脚本仓库更新渠道。"); return; } + + // 获取当前仓库URL + string repoUrl = CurrentRepoUrl; + + // 验证URL + if (string.IsNullOrWhiteSpace(repoUrl)) + { + Toast.Warning("请输入自定义仓库URL。"); + return; + } + + if (repoUrl == "https://example.com/custom-repo") + { + Toast.Warning("请修改默认的自定义URL为有效的仓库地址。"); + return; + } + + if (!Uri.TryCreate(repoUrl, UriKind.Absolute, out _)) + { + Toast.Warning("请输入有效的URL地址。"); + return; + } + try { - // 使用选定渠道的URL进行更新 - string repoUrl = SelectedRepoChannel.Url; - // 显示更新中提示 Toast.Information("正在更新脚本仓库,请耐心等待..."); @@ -182,7 +256,8 @@ public partial class ScriptRepoWindow IsUpdating = true; UpdateProgressValue = 0; UpdateProgressText = "准备更新,请耐心等待..."; - // 执行更新 (repoPath, updated) + + // 执行更新 var (_, updated) = await ScriptRepoUpdater.Instance.UpdateCenterRepoByGit(repoUrl, (path, steps, totalSteps) => { @@ -192,7 +267,6 @@ public partial class ScriptRepoWindow UpdateProgressText = $"{path}"; }); - // 更新结果提示 if (updated) { @@ -205,7 +279,7 @@ public partial class ScriptRepoWindow } catch (Exception ex) { - await MessageBox.ErrorAsync($"更新失败,可尝试重置仓库后重新更新。失败原因:: {ex.Message}"); + await ThemedMessageBox.ErrorAsync($"更新失败,可尝试重置仓库后重新更新。失败原因:{ex.Message}"); } finally { @@ -215,11 +289,77 @@ public partial class ScriptRepoWindow } [RelayCommand] - private void OpenLocalScriptRepo() + private async Task OpenLocalScriptRepo() { - TaskContext.Instance().Config.ScriptConfig.ScriptRepoHintDotVisible = false; - ScriptRepoUpdater.Instance.OpenLocalRepoInWebView(); - Close(); + // 检查是否需要提示用户更新仓库 + var shouldContinue = await CheckAndPromptRepoUpdate(); + if (shouldContinue) + { + TaskContext.Instance().Config.ScriptConfig.ScriptRepoHintDotVisible = false; + ScriptRepoUpdater.Instance.OpenLocalRepoInWebView(); + Close(); + } + } + + /// + /// 检查仓库更新时间并提示用户 + /// + /// 是否继续打开仓库(true: 继续打开, false: 取消操作) + private async Task CheckAndPromptRepoUpdate() + { + TimeSpan timeSinceUpdate; + try + { + // 检查仓库文件夹是否存在 + if (!Directory.Exists(ScriptRepoUpdater.CenterRepoPath)) + { + return true; + } + + // 查找 repo.json 文件 + var repoJsonPath = Directory.GetFiles(ScriptRepoUpdater.CenterRepoPath, "repo.json", SearchOption.AllDirectories).FirstOrDefault(); + if (repoJsonPath == null || !File.Exists(repoJsonPath)) + { + return true; + } + + // 获取 repo.json 文件的最后修改时间 + var repoJsonFile = new FileInfo(repoJsonPath); + DateTime lastUpdateTime = repoJsonFile.LastWriteTime; + + // 检查是否超过 30 天 + timeSinceUpdate = DateTime.Now - lastUpdateTime; + if (timeSinceUpdate.TotalDays <= 30) + { + return true; + } + } + catch + { + // 出现异常时,继续打开仓库 + return true; + } + + // 提示用户更新 + var dialog = new RepoUpdateDialog((int)timeSinceUpdate.TotalDays); + var result = await dialog.ShowDialogAsync(); + + if (result == Wpf.Ui.Controls.MessageBoxResult.Primary) + { + // 用户选择"立即更新" + await UpdateRepo(); + return false; + } + else if (result == Wpf.Ui.Controls.MessageBoxResult.Secondary) + { + // 用户选择"直接打开" + return true; + } + else + { + // 用户关闭对话框(点击 X 或按 ESC) + return false; + } } [RelayCommand] @@ -232,11 +372,11 @@ public partial class ScriptRepoWindow } // 添加确认对话框 - var result = await MessageBox.ShowAsync( + var result = await ThemedMessageBox.ShowAsync( "确定要重置脚本仓库吗?无法正常更新时候可以使用本功能,重置后请重新更新脚本仓库。", "确认重置", MessageBoxButton.YesNo, - MessageBoxImage.Warning); + ThemedMessageBox.MessageBoxIcon.Warning); if (result == MessageBoxResult.Yes) { @@ -462,8 +602,20 @@ public partial class ScriptRepoWindow } catch (Exception ex) { - MessageBox.Show($"无法打开链接: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Warning); + ThemedMessageBox.Warning($"无法打开链接: {ex.Message}", "错误"); } e.Handled = true; } + + /// + /// 处理 PasswordBox 的密码变化事件 + /// + private void PasswordBox_PasswordChanged(object sender, RoutedEventArgs e) + { + if (sender is System.Windows.Controls.PasswordBox passwordBox) + { + // 更新 GitToken 属性,触发自动保存到凭据管理器 + GitToken = passwordBox.Password; + } + } } \ No newline at end of file diff --git a/BetterGenshinImpact/View/Windows/ThemedMessageBox.xaml b/BetterGenshinImpact/View/Windows/ThemedMessageBox.xaml new file mode 100644 index 00000000..9e633e2b --- /dev/null +++ b/BetterGenshinImpact/View/Windows/ThemedMessageBox.xaml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BetterGenshinImpact/View/Windows/ThemedMessageBox.xaml.cs b/BetterGenshinImpact/View/Windows/ThemedMessageBox.xaml.cs new file mode 100644 index 00000000..ad69619c --- /dev/null +++ b/BetterGenshinImpact/View/Windows/ThemedMessageBox.xaml.cs @@ -0,0 +1,356 @@ +using System; +using System.Threading.Tasks; +using System.Windows; +using BetterGenshinImpact.Helpers.Ui; +using Wpf.Ui.Controls; +using MessageBoxButton = System.Windows.MessageBoxButton; +using MessageBoxResult = System.Windows.MessageBoxResult; + +namespace BetterGenshinImpact.View.Windows; + +/// +/// 背景跟随主题的消息对话框,兼容旧 MessageBox API +/// +public partial class ThemedMessageBox : FluentWindow +{ + private MessageBoxResult _result; + private MessageBoxButton _buttonType; + + /// + /// 消息框图标类型 + /// + public enum MessageBoxIcon + { + None, + Information, + Warning, + Error, + Question, + Success + } + + /// + /// 初始化主题色消息对话框 + /// + private ThemedMessageBox() + { + InitializeComponent(); + + // 注册事件 + SourceInitialized += OnSourceInitialized; + Closed += OnClosed; + } + + private void OnSourceInitialized(object? sender, EventArgs e) + { + WindowHelper.TryApplySystemBackdrop(this); + } + + private void OnClosed(object? sender, EventArgs e) + { + // 如果没有明确设置结果,根据按钮类型返回默认的关闭结果 + if (_result == MessageBoxResult.None) + { + _result = _buttonType switch + { + MessageBoxButton.OK => MessageBoxResult.OK, + MessageBoxButton.OKCancel => MessageBoxResult.Cancel, + MessageBoxButton.YesNo => MessageBoxResult.No, + MessageBoxButton.YesNoCancel => MessageBoxResult.Cancel, + _ => MessageBoxResult.None + }; + } + } + + /// + /// 显示对话框并返回结果 + /// + private MessageBoxResult ShowDialogWithResult() + { + _result = MessageBoxResult.None; + ShowDialog(); + return _result; + } + + private void PrimaryButton_Click(object sender, RoutedEventArgs e) + { + // 根据按钮类型返回正确的主按钮结果 + _result = _buttonType switch + { + MessageBoxButton.OK => MessageBoxResult.OK, + MessageBoxButton.OKCancel => MessageBoxResult.OK, + MessageBoxButton.YesNo => MessageBoxResult.Yes, + MessageBoxButton.YesNoCancel => MessageBoxResult.Yes, + _ => MessageBoxResult.OK + }; + Close(); + } + + private void SecondaryButton_Click(object sender, RoutedEventArgs e) + { + // 根据按钮类型返回正确的次按钮结果 + _result = _buttonType switch + { + MessageBoxButton.OKCancel => MessageBoxResult.Cancel, + MessageBoxButton.YesNo => MessageBoxResult.No, + MessageBoxButton.YesNoCancel => MessageBoxResult.No, + _ => MessageBoxResult.Cancel + }; + Close(); + } + + private void CloseButton_Click(object sender, RoutedEventArgs e) + { + // 关闭按钮仅在 YesNoCancel 时显示,始终返回 Cancel + _result = MessageBoxResult.Cancel; + Close(); + } + + /// + /// 显示自定义消息框 + /// + /// 消息内容 + /// 标题 + /// 按钮类型 + /// 图标类型 + /// 默认结果 + /// 父窗口 + /// 用户选择的结果 + /// + /// 此方法必须在 UI 线程上调用。它会阻塞调用线程直到用户关闭对话框。 + /// 对话框使用 ShowDialog() 显示,内部会创建嵌套消息循环来处理用户交互。 + /// 如果需要从非 UI 线程调用,请使用 ShowAsync 方法。 + /// + public static MessageBoxResult Show( + string content, + string title = "提示", + MessageBoxButton button = MessageBoxButton.OK, + MessageBoxIcon icon = MessageBoxIcon.Information, + MessageBoxResult defaultResult = MessageBoxResult.None, + Window? owner = null) + { + var messageBox = new ThemedMessageBox + { + Title = title + }; + + // 设置父窗口,需要防止将自己设置为父窗口,以及处理 MainWindow 未初始化的情况 + if (owner != null && owner != messageBox) + { + messageBox.Owner = owner; + } + else if (Application.Current?.MainWindow != null && Application.Current.MainWindow != messageBox) + { + messageBox.Owner = Application.Current.MainWindow; + } + + // 设置消息内容 + messageBox.MessageTextBlock.Text = content; + + // 设置图标 + SetIcon(messageBox, icon); + + // 设置按钮并保存按钮类型 + messageBox._buttonType = button; + SetButtons(messageBox, button); + + var result = messageBox.ShowDialogWithResult(); + return result == MessageBoxResult.None ? defaultResult : result; + } + + /// + /// 异步显示主题色消息框 + /// + /// 消息内容 + /// 标题 + /// 按钮类型 + /// 图标类型 + /// 默认结果 + /// 父窗口 + /// 用户选择结果的 Task + /// + /// 此方法可以从任何线程安全调用。它会将对话框的显示调度到 UI 线程, + /// 并返回一个 Task 以便调用者可以 await 等待用户响应。 + /// 推荐在异步上下文中使用此方法以避免阻塞调用线程。 + /// + public static Task ShowAsync( + string content, + string title = "提示", + MessageBoxButton button = MessageBoxButton.OK, + MessageBoxIcon icon = MessageBoxIcon.Information, + MessageBoxResult defaultResult = MessageBoxResult.None, + Window? owner = null) + { + return Application.Current.Dispatcher.InvokeAsync(() => + Show(content, title, button, icon, defaultResult, owner)).Task; + } + + /// + /// 设置图标 + /// + private static void SetIcon(ThemedMessageBox messageBox, MessageBoxIcon icon) + { + if (icon == MessageBoxIcon.None) + { + messageBox.MessageIcon.Visibility = Visibility.Collapsed; + messageBox.TitleBar.Icon = null; + return; + } + + var symbol = icon switch + { + MessageBoxIcon.Information => SymbolRegular.Info24, + MessageBoxIcon.Warning => SymbolRegular.Warning24, + MessageBoxIcon.Error => SymbolRegular.ErrorCircle24, + MessageBoxIcon.Question => SymbolRegular.QuestionCircle24, + MessageBoxIcon.Success => SymbolRegular.CheckmarkCircle24, + _ => SymbolRegular.Info24 + }; + + messageBox.MessageIcon.Symbol = symbol; + messageBox.TitleBar.Icon = new SymbolIcon(symbol); + + var colorKey = icon switch + { + MessageBoxIcon.Information => "SystemFillColorAttentionBrush", + MessageBoxIcon.Warning => "SystemFillColorCautionBrush", + MessageBoxIcon.Error => "SystemFillColorCriticalBrush", + MessageBoxIcon.Question => "SystemFillColorNeutralBrush", + MessageBoxIcon.Success => "SystemFillColorSuccessBrush", + _ => "SystemFillColorAttentionBrush" + }; + + if (Application.Current != null) + { + var brush = Application.Current.TryFindResource(colorKey) as System.Windows.Media.Brush; + if (brush != null) + { + messageBox.MessageIcon.Foreground = brush; + messageBox.TitleBar.Icon.Foreground = brush; + } + } + } + + private static void SetButtons(ThemedMessageBox messageBox, MessageBoxButton button) + { + switch (button) + { + case MessageBoxButton.OK: + messageBox.PrimaryButton.Content = "确定"; + messageBox.PrimaryButton.Visibility = Visibility.Visible; + break; + + case MessageBoxButton.OKCancel: + messageBox.PrimaryButton.Content = "确定"; + messageBox.PrimaryButton.Visibility = Visibility.Visible; + messageBox.SecondaryButton.Content = "取消"; + messageBox.SecondaryButton.Visibility = Visibility.Visible; + break; + + case MessageBoxButton.YesNo: + messageBox.PrimaryButton.Content = "是"; + messageBox.PrimaryButton.Visibility = Visibility.Visible; + messageBox.SecondaryButton.Content = "否"; + messageBox.SecondaryButton.Visibility = Visibility.Visible; + break; + + case MessageBoxButton.YesNoCancel: + messageBox.PrimaryButton.Content = "是"; + messageBox.PrimaryButton.Visibility = Visibility.Visible; + messageBox.SecondaryButton.Content = "否"; + messageBox.SecondaryButton.Visibility = Visibility.Visible; + messageBox.CloseButton.Content = "取消"; + messageBox.CloseButton.Visibility = Visibility.Visible; + break; + } + } + + #region Error 方法 + + /// + /// 显示错误消息框(同步,阻塞调用) + /// + public static void Error(string message, string title = "错误") => + Show(message, title, MessageBoxButton.OK, MessageBoxIcon.Error); + + public static MessageBoxResult Error(string message, string title, MessageBoxButton button, MessageBoxResult defaultResult = MessageBoxResult.None) => + Show(message, title, button, MessageBoxIcon.Error, defaultResult); + + public static Task ErrorAsync(string message, string title = "错误") => + ShowAsync(message, title, MessageBoxButton.OK, MessageBoxIcon.Error); + + public static Task ErrorAsync(string message, string title, MessageBoxButton button, MessageBoxResult defaultResult = MessageBoxResult.None) => + ShowAsync(message, title, button, MessageBoxIcon.Error, defaultResult); + + #endregion + + #region Warning 方法 + + /// + /// 显示警告消息框(同步,阻塞调用) + /// + public static void Warning(string message, string title = "警告") => + Show(message, title, MessageBoxButton.OK, MessageBoxIcon.Warning); + + public static MessageBoxResult Warning(string message, string title, MessageBoxButton button, MessageBoxResult defaultResult = MessageBoxResult.None) => + Show(message, title, button, MessageBoxIcon.Warning, defaultResult); + + public static Task WarningAsync(string message, string title = "警告") => + ShowAsync(message, title, MessageBoxButton.OK, MessageBoxIcon.Warning); + + public static Task WarningAsync(string message, string title, MessageBoxButton button, MessageBoxResult defaultResult = MessageBoxResult.None) => + ShowAsync(message, title, button, MessageBoxIcon.Warning, defaultResult); + + #endregion + + #region Information 方法 + + /// + /// 显示信息消息框(同步,阻塞调用) + /// + public static void Information(string message, string title = "信息") => + Show(message, title, MessageBoxButton.OK, MessageBoxIcon.Information); + + public static MessageBoxResult Information(string message, string title, MessageBoxButton button, MessageBoxResult defaultResult = MessageBoxResult.None) => + Show(message, title, button, MessageBoxIcon.Information, defaultResult); + + public static Task InformationAsync(string message, string title = "信息") => + ShowAsync(message, title, MessageBoxButton.OK, MessageBoxIcon.Information); + + public static Task InformationAsync(string message, string title, MessageBoxButton button, MessageBoxResult defaultResult = MessageBoxResult.None) => + ShowAsync(message, title, button, MessageBoxIcon.Information, defaultResult); + + #endregion + + #region Success 方法 + + /// + /// 显示成功消息框(同步,阻塞调用) + /// + public static void Success(string message, string title = "成功") => + Show(message, title, MessageBoxButton.OK, MessageBoxIcon.Success); + + public static MessageBoxResult Success(string message, string title, MessageBoxButton button, MessageBoxResult defaultResult = MessageBoxResult.None) => + Show(message, title, button, MessageBoxIcon.Success, defaultResult); + + public static Task SuccessAsync(string message, string title = "成功") => + ShowAsync(message, title, MessageBoxButton.OK, MessageBoxIcon.Success); + + public static Task SuccessAsync(string message, string title, MessageBoxButton button, MessageBoxResult defaultResult = MessageBoxResult.None) => + ShowAsync(message, title, button, MessageBoxIcon.Success, defaultResult); + + #endregion + + #region Question 方法 + + public static MessageBoxResult Question(string message, string title, MessageBoxButton button, MessageBoxResult defaultResult = MessageBoxResult.None) => + Show(message, title, button, MessageBoxIcon.Question, defaultResult); + + public static Task QuestionAsync(string message, string title = "确认") => + ShowAsync(message, title, MessageBoxButton.YesNo, MessageBoxIcon.Question); + + public static Task QuestionAsync(string message, string title, MessageBoxButton button, MessageBoxResult defaultResult = MessageBoxResult.None) => + ShowAsync(message, title, button, MessageBoxIcon.Question, defaultResult); + + #endregion +} diff --git a/BetterGenshinImpact/View/Windows/WelcomeDialog.xaml.cs b/BetterGenshinImpact/View/Windows/WelcomeDialog.xaml.cs index 6d7f959d..d9767921 100644 --- a/BetterGenshinImpact/View/Windows/WelcomeDialog.xaml.cs +++ b/BetterGenshinImpact/View/Windows/WelcomeDialog.xaml.cs @@ -1,4 +1,5 @@ -using System; +using BetterGenshinImpact.Helpers.Ui; +using System; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; @@ -12,6 +13,7 @@ public partial class WelcomeDialog public WelcomeDialog() { InitializeComponent(); + SourceInitialized += (s, e) => WindowHelper.TryApplySystemBackdrop(this); this.Loaded += WelcomeDialogLoaded; } diff --git a/BetterGenshinImpact/ViewModel/MainWindowViewModel.cs b/BetterGenshinImpact/ViewModel/MainWindowViewModel.cs index d137cbf2..2a34fa24 100644 --- a/BetterGenshinImpact/ViewModel/MainWindowViewModel.cs +++ b/BetterGenshinImpact/ViewModel/MainWindowViewModel.cs @@ -1,4 +1,4 @@ -using BetterGenshinImpact.Core.Config; +using BetterGenshinImpact.Core.Config; using BetterGenshinImpact.Core.Recognition.OCR; using BetterGenshinImpact.Core.Script; using BetterGenshinImpact.GameTask; @@ -8,6 +8,7 @@ using BetterGenshinImpact.Helpers.Ui; using BetterGenshinImpact.Model; using BetterGenshinImpact.Service.Interface; using BetterGenshinImpact.View; +using BetterGenshinImpact.View.Pages; using BetterGenshinImpact.View.Windows; using BetterGenshinImpact.ViewModel.Pages; using CommunityToolkit.Mvvm.ComponentModel; @@ -28,6 +29,8 @@ using System.Net.Http.Json; using System.Threading.Tasks; using System.Windows; using System.Windows.Media; +using BetterGenshinImpact.Helpers.Http; +using BetterGenshinImpact.ViewModel.Windows; using Wpf.Ui; using Wpf.Ui.Controls; @@ -37,6 +40,7 @@ public partial class MainWindowViewModel : ObservableObject, IViewModel { private readonly ILogger _logger; private readonly IConfigService _configService; + private readonly INavigationService _navigationService; public string Title => $"BetterGI · 更好的原神 · {Global.Version}{(RuntimeHelper.IsDebug ? " · Dev" : string.Empty)}"; [ObservableProperty] private bool _isVisible = true; @@ -46,6 +50,10 @@ public partial class MainWindowViewModel : ObservableObject, IViewModel [ObservableProperty] private WindowBackdropType _currentBackdropType = WindowBackdropType.Auto; [ObservableProperty] private bool _isWin11Later = OsVersionHelper.IsWindows11_OrGreater; + + [ObservableProperty] private Brush _redeemCodeButtonForeground = Brushes.White; + + private string? _redeemCodeUpdateNewVersion; private bool _firstActivated = true; @@ -53,6 +61,7 @@ public partial class MainWindowViewModel : ObservableObject, IViewModel public MainWindowViewModel(INavigationService navigationService, IConfigService configService) { + _navigationService = navigationService; _configService = configService; Config = _configService.Get(); _logger = App.GetLogger(); @@ -188,6 +197,12 @@ public partial class MainWindowViewModel : ObservableObject, IViewModel { WindowHelper.ApplyThemeToWindow(Application.Current.MainWindow, themeType); } + + // 根据当前主题更新兑换码按钮的默认前景色(若无更新高亮) + if (_redeemCodeUpdateNewVersion == null) + { + UpdateRedeemCodeButtonDefaultForeground(); + } } [RelayCommand] @@ -200,6 +215,21 @@ public partial class MainWindowViewModel : ObservableObject, IViewModel } } + [RelayCommand] + private void OnOpenFeed() + { + if (_redeemCodeUpdateNewVersion != null) + { + Config.CommonConfig.RedeemCodeFeedsUpdateVersion = _redeemCodeUpdateNewVersion; + // 重置为主题默认前景色,避免浅色主题下显示为白色 + UpdateRedeemCodeButtonDefaultForeground(); + _redeemCodeUpdateNewVersion = null; + } + + var feedWindow = new FeedWindow(new FeedWindowViewModel()); + feedWindow.Show(); + } + [RelayCommand] private async Task OnLoaded() { @@ -242,6 +272,9 @@ public partial class MainWindowViewModel : ObservableObject, IViewModel // 检查更新 await App.GetService()!.CheckUpdateAsync(new UpdateOption()); + + // 检查兑换码更新 + await CheckRedeemCodeFeedsUpdateAsync(); // Win11下 BitBlt截图方式不可用,需要关闭窗口优化功能 if (OsVersionHelper.IsWindows11_OrGreater && TaskContext.Instance().Config.AutoFixWin11BitBlt) @@ -282,7 +315,7 @@ public partial class MainWindowViewModel : ObservableObject, IViewModel { _logger.LogError("首次运行自动初始化按键绑定异常:" + e.Source + "\r\n--" + Environment.NewLine + e.StackTrace + "\r\n---" + Environment.NewLine + e.Message); - MessageBox.Error("读取原神键位并设置键位绑定数据时发生异常:" + e.Message + ",后续可以手动设置"); + await ThemedMessageBox.ErrorAsync("读取原神键位并设置键位绑定数据时发生异常:" + e.Message + ",后续可以手动设置"); } } */ @@ -305,15 +338,15 @@ public partial class MainWindowViewModel : ObservableObject, IViewModel // 低版本才需要迁移 if (fileVersionInfo.FileVersion != null && !Global.IsNewVersion(fileVersionInfo.FileVersion)) { - var res = await MessageBox.ShowAsync("检测到旧的 BetterGI 配置,是否迁移配置并清理旧目录?", "BetterGI", - System.Windows.MessageBoxButton.YesNo, MessageBoxImage.Question); + var res = await ThemedMessageBox.ShowAsync("检测到旧的 BetterGI 配置,是否迁移配置并清理旧目录?", "BetterGI", + System.Windows.MessageBoxButton.YesNo, ThemedMessageBox.MessageBoxIcon.Question); if (res == System.Windows.MessageBoxResult.Yes) { // 迁移配置,拷贝整个目录并覆盖 DirectoryHelper.CopyDirectory(embeddedUserPath, Global.Absolute("User")); // 删除旧目录 DirectoryHelper.DeleteReadOnlyDirectory(embeddedPath); - await MessageBox.InformationAsync("迁移配置成功, 软件将自动退出,请手动重新启动 BetterGI!"); + await ThemedMessageBox.InformationAsync("迁移配置成功, 软件将自动退出,请手动重新启动 BetterGI!"); Application.Current.Shutdown(); } } @@ -379,7 +412,7 @@ public partial class MainWindowViewModel : ObservableObject, IViewModel } catch (Exception e) { - MessageBox.Warning("PaddleOcr预热失败,解决方案:【https://bettergi.com/faq.html】 \r\n" + e.Source + "\r\n--" + + ThemedMessageBox.Warning("PaddleOcr预热失败,解决方案:【https://bettergi.com/faq.html】 \r\n" + e.Source + "\r\n--" + Environment.NewLine + e.StackTrace + "\r\n---" + Environment.NewLine + e.Message); Process.Start( new ProcessStartInfo( @@ -410,4 +443,58 @@ public partial class MainWindowViewModel : ObservableObject, IViewModel _configService.Save(); } } + + private async Task CheckRedeemCodeFeedsUpdateAsync() + { + try + { + var request = new HttpRequestMessage(HttpMethod.Get, "https://cnb.cool/bettergi/genshin-redeem-code/-/git/raw/main/update_time.txt"); + var response = await HttpClientFactory.GetCommonSendClient().SendAsync(request); + response.EnsureSuccessStatusCode(); + var txt = await response.Content.ReadAsStringAsync(); + + + if (!string.IsNullOrEmpty(txt)) + { + if (long.TryParse(txt, out long v2) + && long.TryParse(Config.CommonConfig.RedeemCodeFeedsUpdateVersion, out long v1)) + { + if (v2 > v1) + { + RedeemCodeButtonForeground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#1E9BFA")); + _redeemCodeUpdateNewVersion = txt; + } + } + } + + } + catch (Exception ex) + { + _logger.LogDebug(ex, $"获取兑换码是否存在更新失败"); + } + } + + // 更新兑换码按钮在当前主题下的默认前景色 + private void UpdateRedeemCodeButtonDefaultForeground() + { + try + { + var brush = Application.Current.TryFindResource("TextFillColorPrimaryBrush") as Brush; + if (brush != null) + { + RedeemCodeButtonForeground = brush; + return; + } + } + catch + { + // 忽略资源查找异常,走回退逻辑 + } + + // 回退:根据当前主题类型使用黑/白色 + var isLightTheme = Config.CommonConfig.CurrentThemeType == ThemeType.LightNone + || Config.CommonConfig.CurrentThemeType == ThemeType.LightMica + || Config.CommonConfig.CurrentThemeType == ThemeType.LightAcrylic; + RedeemCodeButtonForeground = isLightTheme ? Brushes.Black : Brushes.White; + } } \ No newline at end of file diff --git a/BetterGenshinImpact/ViewModel/Pages/CommonSettingsPageViewModel.cs b/BetterGenshinImpact/ViewModel/Pages/CommonSettingsPageViewModel.cs index d52622cb..6b6a7745 100644 --- a/BetterGenshinImpact/ViewModel/Pages/CommonSettingsPageViewModel.cs +++ b/BetterGenshinImpact/ViewModel/Pages/CommonSettingsPageViewModel.cs @@ -282,11 +282,11 @@ public partial class CommonSettingsPageViewModel : ViewModel if (Directory.Exists(ScriptRepoUpdater.CenterRepoPathOld)) { DirectoryHelper.CopyDirectory(ScriptRepoUpdater.CenterRepoPathOld, ScriptRepoUpdater.CenterRepoPath); - MessageBox.Information("脚本仓库离线包导入成功!"); + ThemedMessageBox.Information("脚本仓库离线包导入成功!"); } else { - MessageBox.Error("脚本仓库离线包导入失败,不正确的脚本仓库离线包内容!"); + ThemedMessageBox.Error("脚本仓库离线包导入失败,不正确的脚本仓库离线包内容!"); DirectoryHelper.DeleteReadOnlyDirectory(ScriptRepoUpdater.ReposPath); } } @@ -322,7 +322,7 @@ public partial class CommonSettingsPageViewModel : ViewModel [RelayCommand] private async Task CheckUpdateAlphaAsync() { - MessageBoxResult result = await MessageBox.ShowAsync("测试版本非常不稳定!\n测试版本非常不稳定!\n测试版本非常不稳定!\n\n是否继续检查更新?", "警告", System.Windows.MessageBoxButton.YesNo, MessageBoxImage.Exclamation, System.Windows.MessageBoxResult.None); + var result = await ThemedMessageBox.ShowAsync("测试版本非常不稳定!\n测试版本非常不稳定!\n测试版本非常不稳定!\n\n是否继续检查更新?", "警告", MessageBoxButton.YesNo, ThemedMessageBox.MessageBoxIcon.Warning); if (result != MessageBoxResult.Yes) { return; diff --git a/BetterGenshinImpact/ViewModel/Pages/HomePageViewModel.cs b/BetterGenshinImpact/ViewModel/Pages/HomePageViewModel.cs index d06af355..d432880f 100644 --- a/BetterGenshinImpact/ViewModel/Pages/HomePageViewModel.cs +++ b/BetterGenshinImpact/ViewModel/Pages/HomePageViewModel.cs @@ -13,6 +13,7 @@ using BetterGenshinImpact.Service.Interface; using BetterGenshinImpact.View; using BetterGenshinImpact.View.Controls.Webview; using BetterGenshinImpact.View.Pages.View; +using BetterGenshinImpact.View.Windows; using BetterGenshinImpact.ViewModel.Pages.View; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; @@ -20,6 +21,7 @@ using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging.Messages; using Fischless.GameCapture; using Microsoft.Extensions.Logging; +using Microsoft.Win32; using System; using System.Collections.Frozen; using System.Collections.Generic; @@ -35,28 +37,25 @@ using System.Windows; using System.Windows.Controls; using System.Windows.Interop; using System.Windows.Media; +using System.Windows.Media.Imaging; using Windows.System; using Wpf.Ui.Controls; +using Wpf.Ui.Violeta.Controls; namespace BetterGenshinImpact.ViewModel.Pages; public partial class HomePageViewModel : ViewModel { - [ObservableProperty] - private IEnumerable> _modeNames = EnumExtensions.ToEnumItems(); + [ObservableProperty] private IEnumerable> _modeNames = EnumExtensions.ToEnumItems(); - [ObservableProperty] - private string? _selectedMode = CaptureModes.BitBlt.ToString(); + [ObservableProperty] private string? _selectedMode = CaptureModes.BitBlt.ToString(); - [ObservableProperty] - private bool _taskDispatcherEnabled = false; + [ObservableProperty] private bool _taskDispatcherEnabled = false; - [ObservableProperty] - [NotifyCanExecuteChangedFor(nameof(StartTriggerCommand))] + [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(StartTriggerCommand))] private bool _startButtonEnabled = true; - [ObservableProperty] - [NotifyCanExecuteChangedFor(nameof(StopTriggerCommand))] + [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(StopTriggerCommand))] private bool _stopButtonEnabled = true; public AllConfig Config { get; set; } @@ -70,14 +69,19 @@ public partial class HomePageViewModel : ViewModel // 记录上次使用原神的句柄 private IntPtr _hWnd; - [ObservableProperty] - private InferenceDeviceType[] _inferenceDeviceTypes = Enum.GetValues(); + [ObservableProperty] private InferenceDeviceType[] _inferenceDeviceTypes = Enum.GetValues(); + + [ObservableProperty] private ImageSource _bannerImageSource; + + private const string DefaultBannerImagePath = "pack://application:,,,/Resources/Images/banner.jpg"; + private readonly string _customBannerImagePath = Global.Absolute("User/Images/custom_banner.jpg"); public HomePageViewModel(IConfigService configService, TaskTriggerDispatcher taskTriggerDispatcher) { _taskDispatcher = taskTriggerDispatcher; Config = configService.Get(); ReadGameInstallPath(); + InitializeBannerImage(); // WindowsGraphicsCapture 只支持 Win10 18362 及以上的版本 (Windows 10 version 1903 or later) @@ -120,12 +124,13 @@ public partial class HomePageViewModel : ViewModel private void OnLoaded() { // OnTest(); - + // 组件首次加载时运行一次。 if (!_autoRun) { return; } + _autoRun = false; var args = Environment.GetCommandLineArgs(); @@ -174,7 +179,7 @@ public partial class HomePageViewModel : ViewModel } else { - MessageBox.Error("选择的窗体句柄为空"); + ThemedMessageBox.Error("选择的窗体句柄为空"); } } } @@ -192,7 +197,7 @@ public partial class HomePageViewModel : ViewModel } else { - MessageBox.Error("选择的窗体句柄为空!"); + ThemedMessageBox.Error("选择的窗体句柄为空!"); } } } @@ -218,7 +223,7 @@ public partial class HomePageViewModel : ViewModel { if (string.IsNullOrEmpty(Config.GenshinStartConfig.InstallPath)) { - MessageBox.Error("没有找到原神的安装路径"); + await ThemedMessageBox.ErrorAsync("没有找到原神的安装路径"); return; } @@ -227,11 +232,15 @@ public partial class HomePageViewModel : ViewModel { TaskContext.Instance().LinkedStartGenshinTime = DateTime.Now; // 标识关联启动原神的时间 } + else + { + return; + } } if (hWnd == IntPtr.Zero) { - await MessageBox.ErrorAsync("未找到原神窗口,请先启动原神!"); + await ThemedMessageBox.ErrorAsync("未找到原神窗口,请先启动原神!"); return; } } @@ -246,7 +255,7 @@ public partial class HomePageViewModel : ViewModel { if (Config.TriggerInterval <= 0) { - MessageBox.Error("触发器触发频率必须大于0"); + ThemedMessageBox.Error("触发器触发频率必须大于0"); return; } @@ -345,11 +354,11 @@ public partial class HomePageViewModel : ViewModel // new Bitmap(Global.Absolute("test_yolo.png")).Save(memoryStream, ImageFormat.Bmp); // memoryStream.Seek(0, SeekOrigin.Begin); // var result = predictor.Detect(memoryStream); - // MessageBox.Show(JsonSerializer.Serialize(result)); + // ThemedMessageBox.Show(JsonSerializer.Serialize(result)); //} //catch (Exception e) //{ - // MessageBox.Show(e.StackTrace); + // ThemedMessageBox.Show(e.StackTrace); //} // Mat tar = new(@"E:\HuiTask\更好的原神\自动剧情\自动邀约\selected.png", ImreadModes.Grayscale); @@ -466,9 +475,9 @@ public partial class HomePageViewModel : ViewModel var titleBar = new TitleBar { Title = "启动参数说明", - Icon = new ImageIcon - { - Source = new System.Windows.Media.Imaging.BitmapImage(new Uri(@"pack://application:,,,/Resources/Images/logo.png", UriKind.Absolute)) + Icon = new ImageIcon + { + Source = new System.Windows.Media.Imaging.BitmapImage(new Uri(@"pack://application:,,,/Resources/Images/logo.png", UriKind.Absolute)) }, }; System.Windows.Controls.Grid.SetRow(titleBar, 0); @@ -490,23 +499,133 @@ public partial class HomePageViewModel : ViewModel WindowBackdropType = WindowBackdropType.Mica, ExtendsContentIntoTitleBar = true, }; - dialogWindow.SourceInitialized += (s, e) => - { - WindowHelper.TryApplySystemBackdrop(dialogWindow); - }; + dialogWindow.SourceInitialized += (s, e) => WindowHelper.TryApplySystemBackdrop(dialogWindow); dialogWindow.ShowDialog(); } [RelayCommand] public void OnOpenHardwareAccelerationSettings() { - var dialogWindow = new Window + var dialogWindow = new FluentWindow { Title = "硬件加速设置", Content = new HardwareAccelerationView(new HardwareAccelerationViewModel()), SizeToContent = SizeToContent.WidthAndHeight, + ResizeMode = ResizeMode.NoResize, + Owner = Application.Current.MainWindow, WindowStartupLocation = WindowStartupLocation.CenterOwner, + ExtendsContentIntoTitleBar = true, + WindowBackdropType = WindowBackdropType.Auto, }; + dialogWindow.SourceInitialized += (s, e) => WindowHelper.TryApplySystemBackdrop(dialogWindow); var result = dialogWindow.ShowDialog(); } -} + + #region 背景图片管理 + + private void InitializeBannerImage() + { + try + { + // 检查是否存在自定义图片 + if (File.Exists(_customBannerImagePath)) + { + var bitmap = new BitmapImage(); + bitmap.BeginInit(); + bitmap.UriSource = new Uri(Path.GetFullPath(_customBannerImagePath)); + bitmap.CacheOption = BitmapCacheOption.OnLoad; + bitmap.EndInit(); + BannerImageSource = bitmap; + _logger.LogInformation("已加载自定义背景图片"); + } + else + { + // 使用默认图片 + BannerImageSource = new BitmapImage(new Uri(DefaultBannerImagePath, UriKind.Absolute)); + _logger.LogInformation("已加载默认背景图片"); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "初始化背景图片失败,使用默认图片"); + BannerImageSource = new BitmapImage(new Uri(DefaultBannerImagePath, UriKind.Absolute)); + } + } + + [RelayCommand] + private void ChangeBannerImage() + { + try + { + var openFileDialog = new OpenFileDialog + { + Title = "选择背景图片", + Filter = "图片文件|*.jpg;*.jpeg;*.png;*.bmp;*.gif|所有文件|*.*", + Multiselect = false + }; + + if (openFileDialog.ShowDialog() == true) + { + ResetBannerImage(); + + var selectedFile = openFileDialog.FileName; + + // 确保目标目录存在 + var directory = Path.GetDirectoryName(_customBannerImagePath); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + // 复制图片到自定义路径 + File.Copy(selectedFile, _customBannerImagePath, true); + + // 更新UI + var bitmap = new BitmapImage(); + bitmap.BeginInit(); + bitmap.UriSource = new Uri(Path.GetFullPath(_customBannerImagePath)); + bitmap.CacheOption = BitmapCacheOption.OnLoad; + bitmap.EndInit(); + BannerImageSource = bitmap; + Toast.Success("背景图片更换成功!"); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "更换背景图片失败"); + Toast.Error($"更换背景图片失败: {ex.Message}"); + } + } + + [RelayCommand] + private void ResetBannerImage() + { + try + { + // 获取自定义图片的完整路径 + var customImageFullPath = Path.GetFullPath(_customBannerImagePath); + _logger.LogInformation("尝试恢复默认背景图片,自定义图片路径: {CustomPath}", customImageFullPath); + + // 先切换到默认图片,释放自定义图片的文件锁 + var defaultBitmap = new BitmapImage(); + defaultBitmap.BeginInit(); + defaultBitmap.UriSource = new Uri(DefaultBannerImagePath, UriKind.Absolute); + defaultBitmap.CacheOption = BitmapCacheOption.OnLoad; + defaultBitmap.EndInit(); + BannerImageSource = defaultBitmap; + + if (File.Exists(customImageFullPath)) + { + File.Delete(customImageFullPath); + Toast.Success("已恢复为默认背景图片!"); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "恢复默认背景图片失败"); + Toast.Warning("已恢复为默认背景图片!但清除自定义图片失败,请手动删除文件。"); + } + } + + #endregion +} \ No newline at end of file diff --git a/BetterGenshinImpact/ViewModel/Pages/JsListViewModel.cs b/BetterGenshinImpact/ViewModel/Pages/JsListViewModel.cs index 3a09d08c..7bde48a2 100644 --- a/BetterGenshinImpact/ViewModel/Pages/JsListViewModel.cs +++ b/BetterGenshinImpact/ViewModel/Pages/JsListViewModel.cs @@ -135,6 +135,53 @@ public partial class JsListViewModel : ViewModel InitScriptListViewData(); } + [RelayCommand] + public async Task OnDeleteScript(ScriptProject? item) + { + if (item == null) + { + return; + } + + // 显示确认对话框 + var messageBox = new Wpf.Ui.Controls.MessageBox + { + Title = "删除确认", + Content = $"确定要删除脚本 \"{item.Manifest.Name}\" 吗?\n\n此操作将永久删除脚本文件夹及其所有内容,无法恢复!", + PrimaryButtonText = "删除", + CloseButtonText = "取消", + Owner = Application.Current.MainWindow, + WindowStartupLocation = WindowStartupLocation.CenterOwner, + }; + + var result = await messageBox.ShowDialogAsync(); + if (result == Wpf.Ui.Controls.MessageBoxResult.Primary) + { + try + { + // 删除脚本文件夹 + if (Directory.Exists(item.ProjectPath)) + { + Directory.Delete(item.ProjectPath, true); + Toast.Success($"已删除脚本: {item.Manifest.Name}"); + _logger.LogInformation("已删除脚本: {Name} ({Path})", item.Manifest.Name, item.ProjectPath); + + // 刷新列表 + InitScriptListViewData(); + } + else + { + Toast.Warning("脚本目录不存在"); + } + } + catch (Exception ex) + { + Toast.Error($"删除脚本失败: {ex.Message}"); + _logger.LogError(ex, "删除脚本失败"); + } + } + } + [RelayCommand] public void OnGoToJsScriptUrl() { diff --git a/BetterGenshinImpact/ViewModel/Pages/MapPathingViewModel.cs b/BetterGenshinImpact/ViewModel/Pages/MapPathingViewModel.cs index a4ea230a..ad91e3dd 100644 --- a/BetterGenshinImpact/ViewModel/Pages/MapPathingViewModel.cs +++ b/BetterGenshinImpact/ViewModel/Pages/MapPathingViewModel.cs @@ -44,6 +44,9 @@ public partial class MapPathingViewModel : ViewModel [ObservableProperty] private FileTreeNode? _selectNode; + [ObservableProperty] + private bool _isRightClickSelection; + private MapPathingDevWindow? _mapPathingDevWindow; private readonly IScriptService _scriptService; @@ -119,9 +122,14 @@ public partial class MapPathingViewModel : ViewModel } [RelayCommand] - public async Task OnStart() + public async Task OnStart(FileTreeNode? item) { - var item = SelectNode; + // 如果没有传入参数,使用选中的节点 + if (item == null) + { + item = SelectNode; + } + if (item == null) { return; @@ -187,6 +195,86 @@ public partial class MapPathingViewModel : ViewModel InitScriptListViewData(); } + [RelayCommand] + public async Task OnDelete(FileTreeNode? item) + { + // 如果没有传入参数,使用选中的节点 + if (item == null) + { + item = SelectNode; + } + + if (item == null) + { + return; + } + + if (string.IsNullOrEmpty(item.FilePath)) + { + Toast.Warning("无法删除:路径无效"); + return; + } + + // 确定删除的内容类型 + string itemType = item.IsDirectory ? "文件夹" : "文件"; + string itemName = item.FileName ?? "未知项目"; + + // 显示确认对话框 + var messageBox = new Wpf.Ui.Controls.MessageBox + { + Title = "删除确认", + Content = $"确定要删除{itemType} \"{itemName}\" 吗?\n\n此操作将永久删除该{itemType}及其所有内容,无法恢复!", + PrimaryButtonText = "删除", + CloseButtonText = "取消", + Owner = Application.Current.MainWindow, + WindowStartupLocation = WindowStartupLocation.CenterOwner, + }; + + var result = await messageBox.ShowDialogAsync(); + if (result == Wpf.Ui.Controls.MessageBoxResult.Primary) + { + try + { + if (item.IsDirectory) + { + // 删除目录 + if (Directory.Exists(item.FilePath)) + { + Directory.Delete(item.FilePath, true); + Toast.Success($"已删除文件夹: {itemName}"); + _logger.LogInformation("已删除地图追踪文件夹: {Name} ({Path})", itemName, item.FilePath); + } + else + { + Toast.Warning("文件夹不存在"); + } + } + else + { + // 删除文件 + if (File.Exists(item.FilePath)) + { + File.Delete(item.FilePath); + Toast.Success($"已删除文件: {itemName}"); + _logger.LogInformation("已删除地图追踪文件: {Name} ({Path})", itemName, item.FilePath); + } + else + { + Toast.Warning("文件不存在"); + } + } + + // 刷新列表 + InitScriptListViewData(); + } + catch (Exception ex) + { + Toast.Error($"删除失败: {ex.Message}"); + _logger.LogError(ex, "删除地图追踪项失败"); + } + } + } + [RelayCommand] public void OnOpenLocalScriptRepo() { @@ -194,11 +282,19 @@ public partial class MapPathingViewModel : ViewModel ScriptRepoUpdater.Instance.OpenScriptRepoWindow(); } + [RelayCommand] + private void SetRightClickSelection(string isRightClick) + { + IsRightClickSelection = "True".Equals(isRightClick, StringComparison.OrdinalIgnoreCase); + } + [RelayCommand] public void OnOpenPathingDetail() { var item = SelectNode; - if (item == null) + + // 如果是右键点击,不打开抽屉 + if (item == null || IsRightClickSelection) { return; } diff --git a/BetterGenshinImpact/ViewModel/Pages/OneDragonFlowViewModel.cs b/BetterGenshinImpact/ViewModel/Pages/OneDragonFlowViewModel.cs index 91ce8d79..24f83ded 100644 --- a/BetterGenshinImpact/ViewModel/Pages/OneDragonFlowViewModel.cs +++ b/BetterGenshinImpact/ViewModel/Pages/OneDragonFlowViewModel.cs @@ -16,6 +16,7 @@ using BetterGenshinImpact.GameTask; using BetterGenshinImpact.GameTask.Common.Element.Assets; using BetterGenshinImpact.GameTask.Common.Job; using BetterGenshinImpact.Helpers; +using BetterGenshinImpact.Helpers.Ui; using BetterGenshinImpact.Service; using BetterGenshinImpact.Service.Notification; using BetterGenshinImpact.Service.Notification.Model.Enum; @@ -247,6 +248,7 @@ public partial class OneDragonFlowViewModel : ViewModel SizeToContent = SizeToContent.Width , // 确保弹窗根据内容自动调整大小 MaxHeight = 600, }; + uiMessageBox.SourceInitialized += (s, e) => WindowHelper.TryApplySystemBackdrop(uiMessageBox); var result = await uiMessageBox.ShowDialogAsync(); if (result == Wpf.Ui.Controls.MessageBoxResult.Primary) { diff --git a/BetterGenshinImpact/ViewModel/Pages/ScriptControlViewModel.cs b/BetterGenshinImpact/ViewModel/Pages/ScriptControlViewModel.cs index c496248b..be459f5e 100644 --- a/BetterGenshinImpact/ViewModel/Pages/ScriptControlViewModel.cs +++ b/BetterGenshinImpact/ViewModel/Pages/ScriptControlViewModel.cs @@ -120,7 +120,7 @@ public partial class ScriptControlViewModel : ViewModel private void ClearTasks() { // 确认? - var result = MessageBox.Show("是否清空所有任务?", "清空任务", MessageBoxButton.YesNo, MessageBoxImage.Question); + var result = ThemedMessageBox.Question("是否清空所有任务?", "清空任务", MessageBoxButton.YesNo, System.Windows.MessageBoxResult.No); if (result != System.Windows.MessageBoxResult.Yes) { return; @@ -1559,6 +1559,7 @@ public partial class ScriptControlViewModel : ViewModel private List LoadAllKmScripts() { var folder = KeyMouseRecordPageViewModel.ScriptPath; + Directory.CreateDirectory(folder); // 获取所有脚本项目 var files = Directory.GetFiles(folder, "*.*", SearchOption.AllDirectories); @@ -2006,13 +2007,17 @@ public partial class ScriptControlViewModel : ViewModel // // await uiMessageBox.ShowDialogAsync(); - var dialogWindow = new Window + var dialogWindow = new FluentWindow { Title = "配置组设置", Content = new ScriptGroupConfigView(new ScriptGroupConfigViewModel(TaskContext.Instance().Config, SelectedScriptGroup.Config)), SizeToContent = SizeToContent.WidthAndHeight, + ResizeMode = ResizeMode.NoResize, WindowStartupLocation = WindowStartupLocation.CenterOwner, + ExtendsContentIntoTitleBar = true, + WindowBackdropType = WindowBackdropType.Auto, }; + dialogWindow.SourceInitialized += (s, e) => WindowHelper.TryApplySystemBackdrop(dialogWindow); // var dialogWindow = new WpfUiWindow(new ScriptGroupConfigView(SelectedScriptGroup.Config)) // { diff --git a/BetterGenshinImpact/ViewModel/Pages/TaskSettingsPageViewModel.cs b/BetterGenshinImpact/ViewModel/Pages/TaskSettingsPageViewModel.cs index cecfb759..ea85b0da 100644 --- a/BetterGenshinImpact/ViewModel/Pages/TaskSettingsPageViewModel.cs +++ b/BetterGenshinImpact/ViewModel/Pages/TaskSettingsPageViewModel.cs @@ -125,7 +125,6 @@ public partial class TaskSettingsPageViewModel : ViewModel public static List AvatarIndexList = ["", "1", "2", "3", "4"]; - [ObservableProperty] private List _autoMusicLevelList = ["传说", "大师", "困难", "普通", "所有"]; @@ -434,7 +433,7 @@ public partial class TaskSettingsPageViewModel : ViewModel // } // catch (Exception ex) // { - // MessageBox.Error(ex.Message); + // ThemedMessageBox.Error(ex.Message); // } } @@ -469,7 +468,7 @@ public partial class TaskSettingsPageViewModel : ViewModel // } // catch (Exception ex) // { - // MessageBox.Error(ex.Message); + // ThemedMessageBox.Error(ex.Message); // } } @@ -553,9 +552,9 @@ public partial class TaskSettingsPageViewModel : ViewModel } [RelayCommand] - private void OnOpenArtifactSalvageTestOCRWindow() + private async Task OnOpenArtifactSalvageTestOCRWindow() { - OcrDialog ocrDialog = new OcrDialog(0.70, 0.112, 0.275, 0.50, "圣遗物分解", this.Config.AutoArtifactSalvageConfig.JavaScript); + ArtifactOcrDialog ocrDialog = new ArtifactOcrDialog(0.70, 0.112, 0.275, 0.50, "圣遗物分解", this.Config.AutoArtifactSalvageConfig.JavaScript); ocrDialog.ShowDialog(); } diff --git a/BetterGenshinImpact/ViewModel/Windows/Editable/ScriptGroupProjectEditorViewModel.cs b/BetterGenshinImpact/ViewModel/Windows/Editable/ScriptGroupProjectEditorViewModel.cs index 6938617c..c3c42124 100644 --- a/BetterGenshinImpact/ViewModel/Windows/Editable/ScriptGroupProjectEditorViewModel.cs +++ b/BetterGenshinImpact/ViewModel/Windows/Editable/ScriptGroupProjectEditorViewModel.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.ComponentModel; using System.Text.Json.Serialization; +using System.Windows.Documents; +using System.Windows.Media; using BetterGenshinImpact.Core.Script.Group; using BetterGenshinImpact.GameTask; using BetterGenshinImpact.Service.Notification; @@ -35,6 +37,79 @@ public class ScriptGroupProjectEditorViewModel : ObservableObject } } + public bool? AllowJsHTTP + { + get + { + return _project.AllowJsHTTP; + } + set + { + // 为了避免误用,AllowJsHTTP禁止set,通过更新Hash来控制 + // 脚本作者更新时,如果Hash变更会自动禁用http权限,避免安全风险 + if (value == null || value == false) + { + _project.AllowJsHTTPHash = null; + } + else + { + _project.AllowJsHTTPHash = _project.GetHttpAllowedUrlsHash(); + } + OnPropertyChanged(); + } + } + + public record JsText(string Text, Brush Color); + public List JsHTTPInfoText + { + get + { + if (_project.Project == null) + { + _project.BuildScriptProjectRelation(); + } + if (_project.Project == null) + { + return new List + { + new JsText("当前脚本项目未加载", Brushes.Red) + }; + } + var urls = _project.Project.Manifest?.HttpAllowedUrls ?? []; + if (urls.Length == 0) + { + return new List + { + new JsText("当前脚本无需使用HTTP资源", Brushes.Green) + }; + } + return new List + { + new JsText($"当前脚本使用 {urls.Length} 个HTTP资源", Brushes.OrangeRed) + }; + } + } + + public record JsLine(string Text, Brush Color); + + public List JsHTTPInfo + { + get + { + if (_project.Project == null) + { + _project.BuildScriptProjectRelation(); + } + var urls = _project.Project?.Manifest?.HttpAllowedUrls ?? []; + var blocks = new List(); + foreach (var url in urls) + { + blocks.Add(new JsLine(url, Brushes.OrangeRed)); + } + return blocks; + } + } + public string Status { get => _project.Status; @@ -58,6 +133,10 @@ public class ScriptGroupProjectEditorViewModel : ObservableObject { OnPropertyChanged(nameof(AllowJsNotification)); } + if (e.PropertyName == nameof(ScriptGroupProject.AllowJsHTTPHash)) + { + OnPropertyChanged(nameof(AllowJsHTTP)); + } }; } } diff --git a/BetterGenshinImpact/ViewModel/Windows/FeedWindowViewModel.cs b/BetterGenshinImpact/ViewModel/Windows/FeedWindowViewModel.cs new file mode 100644 index 00000000..01764c43 --- /dev/null +++ b/BetterGenshinImpact/ViewModel/Windows/FeedWindowViewModel.cs @@ -0,0 +1,168 @@ +using BetterGenshinImpact.ViewModel; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using System.Windows; +using BetterGenshinImpact.GameTask; +using BetterGenshinImpact.GameTask.UseRedeemCode; +using BetterGenshinImpact.Helpers; +using BetterGenshinImpact.Helpers.Http; +using Newtonsoft.Json; +using Wpf.Ui.Violeta.Controls; + +namespace BetterGenshinImpact.ViewModel.Windows; + +public partial class FeedWindowViewModel : ViewModel +{ + [ObservableProperty] private ObservableCollection _feedItems = new(); + [ObservableProperty] private bool _isLoading; + [ObservableProperty] private bool _isDisplayBtnGetLiveCodes; + + private readonly HttpClient _httpClient = HttpClientFactory.GetCommonSendClient(); + private const string CodesJsonUrl = "https://cnb.cool/bettergi/genshin-redeem-code/-/git/raw/main/codes.json"; + + public FeedWindowViewModel() + { + IsDisplayBtnGetLiveCodes = GamePreviewLiveDateCalculator.IsWithinPreviewRange(DateTime.Now); + } + + [RelayCommand] + private async Task GetLiveRedeemCodes() + { + IsLoading = true; + try + { + var getter = new GetLiveRedeemCode(); + var codeList = await getter.GetCodeMsgAsync(); + + if (codeList.Count == 0) + { + Toast.Warning("暂无前瞻兑换码信息"); + return; + } + + var displayItems = codeList + .Select(c => string.IsNullOrWhiteSpace(c.Items) ? null : c.Items) + .Where(s => !string.IsNullOrWhiteSpace(s)) + .ToList(); + + var item = new FeedItem + { + Title = "【实时获取】前瞻直播兑换码", + Content = displayItems.Count > 0 ? string.Join("\n", displayItems) : string.Empty, + Time = DateTime.Now.ToString("yyyy-MM-dd HH:mm"), + Codes = codeList.Select(c => c.Code).ToList() + }; + + // 插入到列表顶部,方便查看 + if (FeedItems.Count > 0 && FeedItems[0].Title == item.Title) + { + // 如果已经存在相同标题的项,则更新内容和时间 + FeedItems[0].Content = item.Content; + FeedItems[0].Time = item.Time; + FeedItems[0].Codes = item.Codes; + } + else + { + FeedItems.Insert(0, item); + } + + + Toast.Success("已实时获取前瞻兑换码"); + } + catch (Exception ex) + { + Toast.Error($"获取前瞻兑换码失败: {ex.Message}"); + } + finally + { + IsLoading = false; + } + } + + [RelayCommand] + private async Task Refresh() + { + await LoadRemoteDataAsync(); + } + + [RelayCommand] + private void CopyItemCodes(FeedItem item) + { + try + { + if (item?.Codes != null && item.Codes.Any()) + { + var codes = string.Join("\n", item.Codes); + UIDispatcherHelper.Invoke(() => Clipboard.SetDataObject(codes)); + RedeemCodeManager.AddNotDetectClipboardText(codes); + Toast.Information("兑换码已复制到剪贴板"); + } + } + catch (Exception ex) + { + Toast.Error($"复制兑换码失败: {ex.Message}"); + } + } + + [RelayCommand] + private async Task AutoRedeemItem(FeedItem item) + { + if (item?.Codes != null && item.Codes.Count != 0) + { + await new TaskRunner().RunSoloTaskAsync(new UseRedemptionCodeTask(item.Codes)); + } + } + + public async Task LoadRemoteDataAsync() + { + IsLoading = true; + try + { + var request = new HttpRequestMessage(HttpMethod.Get, CodesJsonUrl); + var response = await _httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + var json = await response.Content.ReadAsStringAsync(); + + var items = JsonConvert.DeserializeObject>(json) ?? []; + + FeedItems.Clear(); + foreach (var feed in items) + { + // 若存在标签文本,设置 HasTag + feed.HasTag = !string.IsNullOrWhiteSpace(feed.Tag); + FeedItems.Add(feed); + } + } + catch (Exception ex) + { + Toast.Error($"获取兑换码失败: {ex.Message}"); + } + finally + { + IsLoading = false; + } + } +} + +public partial class FeedItem : ObservableObject +{ + [ObservableProperty] private string _title = string.Empty; + + [ObservableProperty] private string _content = string.Empty; + + [ObservableProperty] private string _time = string.Empty; + + [ObservableProperty] private string _tag = string.Empty; + + [ObservableProperty] private bool _hasTag = false; + + [ObservableProperty] private List _codes = new(); + + [ObservableProperty] private string _valid = string.Empty; +} \ No newline at end of file diff --git a/BetterGenshinImpact/ViewModel/Windows/JsonMonoViewModel.cs b/BetterGenshinImpact/ViewModel/Windows/JsonMonoViewModel.cs index 3ca94edb..ae143356 100644 --- a/BetterGenshinImpact/ViewModel/Windows/JsonMonoViewModel.cs +++ b/BetterGenshinImpact/ViewModel/Windows/JsonMonoViewModel.cs @@ -32,7 +32,7 @@ public partial class JsonMonoViewModel : ObservableObject } catch (Exception e) { - MessageBox.Error("读取黑白名单出错:" + e.ToString()); + ThemedMessageBox.Error("读取黑白名单出错:" + e.ToString()); } } @@ -45,7 +45,7 @@ public partial class JsonMonoViewModel : ObservableObject } catch (Exception e) { - MessageBox.Error("保存失败:" + e.ToString()); + ThemedMessageBox.Error("保存失败:" + e.ToString()); return; } @@ -56,7 +56,7 @@ public partial class JsonMonoViewModel : ObservableObject } catch (Exception e) { - MessageBox.Error("保存失败:" + e.ToString()); + ThemedMessageBox.Error("保存失败:" + e.ToString()); } } diff --git a/BetterGenshinImpact/ViewModel/Windows/MapViewerViewModel.cs b/BetterGenshinImpact/ViewModel/Windows/MapViewerViewModel.cs index 0e99d64b..20605a68 100644 --- a/BetterGenshinImpact/ViewModel/Windows/MapViewerViewModel.cs +++ b/BetterGenshinImpact/ViewModel/Windows/MapViewerViewModel.cs @@ -14,6 +14,7 @@ using BetterGenshinImpact.GameTask.Common.Map; using BetterGenshinImpact.Helpers; using System.Linq; using BetterGenshinImpact.Core.Recognition.OpenCv; +using BetterGenshinImpact.GameTask; using BetterGenshinImpact.GameTask.Common.Map.Maps; using BetterGenshinImpact.GameTask.Common.Map.Maps.Base; @@ -43,12 +44,13 @@ public partial class MapViewerViewModel : ObservableObject { if (string.IsNullOrEmpty(mapName)) { - mapName = MapTypes.Teyvat.ToString(); + mapName = nameof(MapTypes.Teyvat); } _mapName = mapName; Init(mapName); - var center = MapManager.GetMap(_mapName).ConvertGenshinMapCoordinatesToImageCoordinates(512, 512); + var matchingMethod = TaskContext.Instance().Config.PathingConditionConfig.MapMatchingMethod; + var center = MapManager.GetMap(_mapName, matchingMethod).ConvertGenshinMapCoordinatesToImageCoordinates(512, 512); _mapBitmap = ClipMat(new Point2f(center.x, center.y)).ToWriteableBitmap(); WeakReferenceMessenger.Default.Register>(this, (sender, msg) => { @@ -208,7 +210,8 @@ public partial class MapViewerViewModel : ObservableObject private Point ConvertToMapPoint(Waypoint point) { - var (x, y) = MapManager.GetMap(_mapName).ConvertGenshinMapCoordinatesToImageCoordinates((float)point.X, (float)point.Y); + var matchingMethod = TaskContext.Instance().Config.PathingConditionConfig.MapMatchingMethod; + var (x, y) = MapManager.GetMap(_mapName, matchingMethod).ConvertGenshinMapCoordinatesToImageCoordinates((float)point.X, (float)point.Y); return new Point(x, y); } diff --git a/Test/BetterGenshinImpact.Test/Cv/ImageDifferenceDetectorTest.cs b/Test/BetterGenshinImpact.Test/Cv/ImageDifferenceDetectorTest.cs new file mode 100644 index 00000000..f6ffab02 --- /dev/null +++ b/Test/BetterGenshinImpact.Test/Cv/ImageDifferenceDetectorTest.cs @@ -0,0 +1,19 @@ +using BetterGenshinImpact.Core.Recognition.OpenCv; +using OpenCvSharp; + +namespace BetterGenshinImpact.Test.Cv; + +public class ImageDifferenceDetectorTest +{ + public static void Test() + { + int i = ImageDifferenceDetector.FindMostDifferentImage([ + Cv2.ImRead(@"E:\1.png", ImreadModes.Grayscale), + Cv2.ImRead(@"E:\2.png", ImreadModes.Grayscale), + Cv2.ImRead(@"E:\3.png", ImreadModes.Grayscale), + Cv2.ImRead(@"E:\4.png", ImreadModes.Grayscale) + ]); + + Console.WriteLine($"差异最大的图片索引是: {i}"); + } +} \ No newline at end of file diff --git a/Test/BetterGenshinImpact.Test/Cv/ImagePixelPrint.cs b/Test/BetterGenshinImpact.Test/Cv/ImagePixelPrint.cs new file mode 100644 index 00000000..ce1cfef9 --- /dev/null +++ b/Test/BetterGenshinImpact.Test/Cv/ImagePixelPrint.cs @@ -0,0 +1,200 @@ +using System.Diagnostics; +using OpenCvSharp; + +namespace BetterGenshinImpact.Test.Cv; + +public class ImagePixelPrint +{ + public static void Test() + { + PrintColorAndGrayValues(@"E:\HuiTask\更好的原神\自动秘境\自动战斗\队伍识别\111.png"); + } + + /// + /// 打印图片的所有颜色值(去重并统计次数),然后灰度化后输出所有灰度值(去重并统计次数) + /// + /// 输入图片 + public static void PrintColorAndGrayValues(Mat inputImage) + { + if (inputImage == null || inputImage.Empty()) + { + Debug.WriteLine("输入图片为空或无效"); + return; + } + + int width = inputImage.Width; + int height = inputImage.Height; + int channels = inputImage.Channels(); + + Debug.WriteLine($"====== 图片信息 ======"); + Debug.WriteLine($"宽度: {width}, 高度: {height}, 通道数: {channels}"); + Debug.WriteLine($"图片类型: {inputImage.Type()}"); + Debug.WriteLine(""); + + // 打印原始图片的颜色值 + Debug.WriteLine("====== 原始图片颜色值(去重并统计) ======"); + + if (channels == 3) // BGR 彩色图片 + { + Dictionary colorCount = new Dictionary(); + + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + Vec3b color = inputImage.At(y, x); + string colorKey = $"B={color.Item0}, G={color.Item1}, R={color.Item2}"; + + if (colorCount.ContainsKey(colorKey)) + { + colorCount[colorKey]++; + } + else + { + colorCount[colorKey] = 1; + } + } + } + + foreach (var kvp in colorCount.OrderByDescending(x => x.Value)) + { + Debug.WriteLine($"{kvp.Key} - 出现次数: {kvp.Value}"); + } + + Debug.WriteLine($"唯一颜色数量: {colorCount.Count}"); + } + else if (channels == 4) // BGRA 彩色图片(带透明度) + { + Dictionary colorCount = new Dictionary(); + + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + Vec4b color = inputImage.At(y, x); + string colorKey = $"B={color.Item0}, G={color.Item1}, R={color.Item2}, A={color.Item3}"; + + if (colorCount.ContainsKey(colorKey)) + { + colorCount[colorKey]++; + } + else + { + colorCount[colorKey] = 1; + } + } + } + + foreach (var kvp in colorCount.OrderByDescending(x => x.Value)) + { + Debug.WriteLine($"{kvp.Key} - 出现次数: {kvp.Value}"); + } + + Debug.WriteLine($"唯一颜色数量: {colorCount.Count}"); + } + else if (channels == 1) // 灰度图片 + { + Dictionary grayCount = new Dictionary(); + + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + byte gray = inputImage.At(y, x); + + if (grayCount.ContainsKey(gray)) + { + grayCount[gray]++; + } + else + { + grayCount[gray] = 1; + } + } + } + + foreach (var kvp in grayCount.OrderByDescending(x => x.Value)) + { + Debug.WriteLine($"Gray={kvp.Key} - 出现次数: {kvp.Value}"); + } + + Debug.WriteLine($"唯一灰度值数量: {grayCount.Count}"); + } + + Debug.WriteLine(""); + + // 转换为灰度图 + Mat grayImage = new Mat(); + if (channels == 3 || channels == 4) + { + Cv2.CvtColor(inputImage, grayImage, ColorConversionCodes.BGR2GRAY); + } + else if (channels == 1) + { + grayImage = inputImage.Clone(); + } + else + { + Debug.WriteLine("不支持的图片格式"); + return; + } + + // 打印灰度化后的值 + Debug.WriteLine("====== 灰度化后的值(去重并统计) ======"); + Dictionary grayValueCount = new Dictionary(); + + for (int y = 0; y < grayImage.Height; y++) + { + for (int x = 0; x < grayImage.Width; x++) + { + byte grayValue = grayImage.At(y, x); + + if (grayValueCount.ContainsKey(grayValue)) + { + grayValueCount[grayValue]++; + } + else + { + grayValueCount[grayValue] = 1; + } + } + } + + foreach (var kvp in grayValueCount.OrderByDescending(x => x.Value)) + { + Debug.WriteLine($"Gray={kvp.Key} - 出现次数: {kvp.Value}"); + } + + Debug.WriteLine($"唯一灰度值数量: {grayValueCount.Count}"); + + Debug.WriteLine("====== 完成 ======"); + + // 释放资源 + grayImage?.Dispose(); + } + + /// + /// 打印图片的所有颜色值,然后灰度化后输出所有灰度值(从文件路径加载) + /// + /// 图片文件路径 + public static void PrintColorAndGrayValues(string imagePath) + { + if (string.IsNullOrEmpty(imagePath)) + { + Debug.WriteLine("图片路径为空"); + return; + } + + Mat image = Cv2.ImRead(imagePath); + if (image.Empty()) + { + Debug.WriteLine($"无法加载图片: {imagePath}"); + return; + } + + Debug.WriteLine($"加载图片: {imagePath}"); + PrintColorAndGrayValues(image); + + image?.Dispose(); + } +} \ No newline at end of file diff --git a/Test/BetterGenshinImpact.Test/Cv/ThresholdWindow.cs b/Test/BetterGenshinImpact.Test/Cv/ThresholdWindow.cs index eb37d2eb..30138d41 100644 --- a/Test/BetterGenshinImpact.Test/Cv/ThresholdWindow.cs +++ b/Test/BetterGenshinImpact.Test/Cv/ThresholdWindow.cs @@ -12,7 +12,17 @@ public class ThresholdWindow public static void Test() { var window = new ThresholdWindow(); - window.ShowThresholdAdjuster(@"E:\HuiTask\更好的原神\自动拾取\pick_ocr_ori_20250915011455192.png"); + window.ShowThresholdAdjuster(@"E:\HuiTask\更好的原神\自动秘境\自动战斗\队伍识别\当前角色小三角\无法识别小三角2.png"); + } + + public static void Save() + { + var image = Cv2.ImRead(@"E:\HuiTask\更好的原神\自动秘境\自动战斗\队伍识别\22.png"); + var grayImage = new Mat(); + Cv2.CvtColor(image, grayImage, ColorConversionCodes.BGR2GRAY); + var thresholdImage = new Mat(); + Cv2.Threshold(grayImage, thresholdImage, 200, 255, ThresholdTypes.Binary); + Cv2.ImWrite(@"E:\HuiTask\更好的原神\自动秘境\自动战斗\队伍识别\22-threshold.png", thresholdImage); } /// diff --git a/Test/BetterGenshinImpact.Test/Dataset/AvatarClassifyGen.cs b/Test/BetterGenshinImpact.Test/Dataset/AvatarClassifyGen.cs index 4979419a..d21cf292 100644 --- a/Test/BetterGenshinImpact.Test/Dataset/AvatarClassifyGen.cs +++ b/Test/BetterGenshinImpact.Test/Dataset/AvatarClassifyGen.cs @@ -7,7 +7,7 @@ namespace BetterGenshinImpact.Test.Dataset; public class AvatarClassifyGen { // 基础图像文件夹 - private const string BaseDir = @"E:\HuiTask\更好的原神\侧面头像\源数据\AvatarIcon"; + private const string BaseDir = @"E:\HuiTask\更好的原神\侧面头像\side_src"; // 产出文件夹 private const string OutputDir = @"E:\HuiTask\更好的原神\侧面头像\dateset"; @@ -17,19 +17,19 @@ public class AvatarClassifyGen private static readonly Random Rd = new Random(); - public static readonly List ImgNames = ["UI_AvatarIcon_Side_Aino.png","UI_AvatarIcon_Side_Flins.png", "UI_AvatarIcon_Side_Nefer.png", "UI_AvatarIcon_Side_Nefer.png", "UI_AvatarIcon_Side_Lauma.png"]; + // public static readonly List ImgNames = ["UI_AvatarIcon_Side_Aino.png","UI_AvatarIcon_Side_Flins.png", "UI_AvatarIcon_Side_Nefer.png", "UI_AvatarIcon_Side_Nefer.png", "UI_AvatarIcon_Side_Lauma.png"]; public static void GenAll() { // 读取基础图像 - // List sideImageFiles = Directory.GetFiles(Path.Combine(BaseDir, "side_src"), "*.png", SearchOption.TopDirectoryOnly).ToList(); + List sideImageFiles = Directory.GetFiles(BaseDir, "*.png", SearchOption.TopDirectoryOnly).ToList(); // 只用一个图像 - List sideImageFiles = []; - - foreach (string imgName in ImgNames) - { - sideImageFiles.Add(Path.Combine(BaseDir, imgName)); - } + // List sideImageFiles = []; + // + // foreach (string imgName in ImgNames) + // { + // sideImageFiles.Add(Path.Combine(BaseDir, imgName)); + // } // 生成训练集 GenTo(sideImageFiles, Path.Combine(OutputDir, @"dateset\train"), 200); // 生成测试集 diff --git a/Test/BetterGenshinImpact.Test/Dataset/AvatarClassifyTransparentGen.cs b/Test/BetterGenshinImpact.Test/Dataset/AvatarClassifyTransparentGen.cs index e011b517..9c84af91 100644 --- a/Test/BetterGenshinImpact.Test/Dataset/AvatarClassifyTransparentGen.cs +++ b/Test/BetterGenshinImpact.Test/Dataset/AvatarClassifyTransparentGen.cs @@ -7,7 +7,7 @@ namespace BetterGenshinImpact.Test.Dataset; public class AvatarClassifyTransparentGen { // 基础图像文件夹 - private const string BaseDir = @"E:\HuiTask\更好的原神\侧面头像\源数据\AvatarIcon"; + private const string BaseDir = @"E:\HuiTask\更好的原神\侧面头像\side_src"; // 产出文件夹 private const string OutputDir = @"E:\HuiTask\更好的原神\侧面头像\dateset"; @@ -22,12 +22,16 @@ public class AvatarClassifyTransparentGen public static void GenAll() { - List sideImageFiles = []; - List imgNames = AvatarClassifyGen.ImgNames; - foreach (string imgName in imgNames) - { - sideImageFiles.Add(Path.Combine(BaseDir, imgName)); - } + // List sideImageFiles = []; + // List imgNames = AvatarClassifyGen.ImgNames; + // foreach (string imgName in imgNames) + // { + // sideImageFiles.Add(Path.Combine(BaseDir, imgName)); + // } + + // 度文件夹下所有 + List sideImageFiles = Directory.GetFiles(BaseDir, "*.png", SearchOption.TopDirectoryOnly).ToList(); + var newList = AdjustTransparency(sideImageFiles, 0.5f); diff --git a/Test/BetterGenshinImpact.UnitTest/Assets b/Test/BetterGenshinImpact.UnitTest/Assets index 1ac6fa09..a1965670 160000 --- a/Test/BetterGenshinImpact.UnitTest/Assets +++ b/Test/BetterGenshinImpact.UnitTest/Assets @@ -1 +1 @@ -Subproject commit 1ac6fa09609d843245109aa405402bcf51cb5bee +Subproject commit a1965670874cb937060bcbf149a1b309e2ffd65d diff --git a/Test/BetterGenshinImpact.UnitTest/BetterGenshinImpact.UnitTest.csproj b/Test/BetterGenshinImpact.UnitTest/BetterGenshinImpact.UnitTest.csproj index 7f03e5e7..3d471106 100644 --- a/Test/BetterGenshinImpact.UnitTest/BetterGenshinImpact.UnitTest.csproj +++ b/Test/BetterGenshinImpact.UnitTest/BetterGenshinImpact.UnitTest.csproj @@ -26,4 +26,8 @@ + + + + diff --git a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoArtifactSalvageTests/AutoArtifactSalvageTaskTests.cs b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoArtifactSalvageTests/AutoArtifactSalvageTaskTests.cs index ae11fd79..5b9ff091 100644 --- a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoArtifactSalvageTests/AutoArtifactSalvageTaskTests.cs +++ b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoArtifactSalvageTests/AutoArtifactSalvageTaskTests.cs @@ -1,7 +1,9 @@ using BetterGenshinImpact.GameTask.AutoArtifactSalvage; using BetterGenshinImpact.GameTask.Model.GameUI; using BetterGenshinImpact.UnitTest.CoreTests.RecognitionTests.OCRTests; +using Microsoft.ClearScript; using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Time.Testing; using OpenCvSharp; using System; using System.Collections.Concurrent; @@ -171,12 +173,18 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoArtifactSalvageTests 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), [ + 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") }; + yield return new object[] { "202510311559_GetArtifactStat.png", new ArtifactStat("黃金乐曲的变奏",/* 应为"黄"*/ new ArtifactAffix(ArtifactAffixType.HP, 717f), [ + new ArtifactAffix(ArtifactAffixType.DEF, 16), + new ArtifactAffix(ArtifactAffixType.ElementalMastery, 16), + new ArtifactAffix(ArtifactAffixType.ATKPercent, 4.1f), + new ArtifactAffix(ArtifactAffixType.HPPercent, 4.7f) + ], 0), new CultureInfo("zh-Hans") }; } } @@ -219,18 +227,16 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoArtifactSalvageTests } [Theory] - [InlineData(@"ArtifactAffixes.png", @"(async function (artifact) { - var hasATK = Array.from(artifact.MinorAffixes).some(affix => affix.Type == 'ATK'); - var hasDEF = Array.from(artifact.MinorAffixes).some(affix => affix.Type == 'DEF'); - Output = hasATK && hasDEF; - })(ArtifactStat);", false)] - [InlineData(@"ArtifactAffixes.png", @"(async function (artifact) { - var level = artifact.Level; - var hasATKPercent = Array.from(artifact.MinorAffixes).some(affix => affix.Type == 'ATKPercent'); - var hasDEF = Array.from(artifact.MinorAffixes).some(affix => affix.Type == 'DEF'); - Output = level == 0 && hasATKPercent && hasDEF; - })(ArtifactStat);", true)] - public void IsMatchJavaScript_JSShouldBeRight(string screenshot, string js, bool expected) + [InlineData(@"ArtifactAffixes.png", @" + var hasATK = Array.from(ArtifactStat.MinorAffixes).some(affix => affix.Type == 'ATK'); + var hasDEF = Array.from(ArtifactStat.MinorAffixes).some(affix => affix.Type == 'DEF'); + Output = hasATK && hasDEF;", false)] + [InlineData(@"ArtifactAffixes.png", @" + var level = ArtifactStat.Level; + var hasATKPercent = Array.from(ArtifactStat.MinorAffixes).some(affix => affix.Type == 'ATKPercent'); + var hasDEF = Array.from(ArtifactStat.MinorAffixes).some(affix => affix.Type == 'DEF'); + Output = level == 0 && hasATKPercent && hasDEF;", true)] + public async Task IsMatchJavaScript_JSShouldBeRight(string screenshot, string js, bool expected) { // using Mat mat = new Mat(@$"..\..\..\Assets\AutoArtifactSalvage\{screenshot}"); @@ -239,10 +245,29 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoArtifactSalvageTests // AutoArtifactSalvageTask sut = new AutoArtifactSalvageTask(new AutoArtifactSalvageTaskParam(5, null, null, null, null, cultureInfo, this.stringLocalizer), new FakeLogger()); ArtifactStat artifact = sut.GetArtifactStat(mat, paddle.Get(), out string _); - bool result = IsMatchJavaScript(artifact, js); + bool result = await IsMatchJavaScript(artifact, js, new FakeLogger()); // Assert.Equal(expected, result); } + + /// + /// 测试JavaScript运行超时的情况,应抛出正确的异常 + /// + /// + [Fact] + public async Task IsMatchJavaScript_Timeout_ShouldThrowException() + { + // + string js = @"while (true) {};"; + FakeTimeProvider timeProvider = new FakeTimeProvider(); + + // + Task sut = IsMatchJavaScript(new ArtifactStat("", new ArtifactAffix(ArtifactAffixType.ATK, 0), [], 0), js, new FakeLogger(), timeProvider); + timeProvider.Advance(TimeSpan.FromSeconds(3)); + + // + await Assert.ThrowsAsync(() => sut); + } } } diff --git a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFightTests/PartyAvatarInitTests.cs b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFightTests/PartyAvatarInitTests.cs new file mode 100644 index 00000000..15db965f --- /dev/null +++ b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFightTests/PartyAvatarInitTests.cs @@ -0,0 +1,108 @@ +using BetterGenshinImpact.Core.Recognition.ONNX; +using BetterGenshinImpact.GameTask.AutoFight; +using BetterGenshinImpact.GameTask.AutoFight.Model; +using BetterGenshinImpact.GameTask.Model; +using Microsoft.Extensions.Configuration; +using OpenCvSharp; + +namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFightTests; + +/// +/// 角色编号,角色头像识别测试 +/// +public class PartyAvatarInitTests +{ +#pragma warning disable CS8618 // 在退出构造函数时,不可为 null 的字段必须包含非 null 值。请考虑添加 "required" 修饰符或声明为可为 null。 + private static BgiYoloPredictor predictor; +#pragma warning restore CS8618 // 在退出构造函数时,不可为 null 的字段必须包含非 null 值。请考虑添加 "required" 修饰符或声明为可为 null。 + private static BgiYoloPredictor Predictor + { + get + { + return LazyInitializer.EnsureInitialized(ref predictor, + () => new BgiOnnxFactory(new FakeLogger()) + .CreateYoloPredictor(BgiOnnxModel.BgiAvatarSide)); + } + } + internal class AutoFightAssets : GameTask.AutoFight.Assets.AutoFightAssets + { + internal AutoFightAssets(ISystemInfo systemInfo) : base(systemInfo) + { + } + } + internal class ElementAssets : GameTask.Common.Element.Assets.ElementAssets + { + internal ElementAssets(ISystemInfo systemInfo) : base(systemInfo) + { + } + } + + /// + /// 测试普通的多人游戏下的角色头像识别 + /// + [Theory] + [InlineData(@"AutoFight\联机满编\别人进我世界_2人.png", new[] { "阿蕾奇诺", "钟离" })] + [InlineData(@"AutoFight\联机满编\别人进我世界_3人.png", new[] { "阿蕾奇诺", "钟离" })] + [InlineData(@"AutoFight\联机满编\别人进我世界_4人.png", new[] { "阿蕾奇诺" })] + [InlineData(@"AutoFight\联机满编\别人进我世界_4人_2.png", new[] { "阿蕾奇诺" })] + [InlineData(@"AutoFight\联机满编\我进别人世界_2人.png", new[] { "阿蕾奇诺", "钟离" })] + [InlineData(@"AutoFight\联机满编\我进别人世界_3人.png", new[] { "阿蕾奇诺" })] + [InlineData(@"AutoFight\联机满编\我进别人世界_4人.png", new[] { "阿蕾奇诺" })] + // 以下5条测试在Assets中无数据 + //[InlineData(@"AutoFight\可识别异常场景\单人队.png", new[] { "茜特菈莉" })] + //[InlineData(@"AutoFight\可识别异常场景\三人队.png", new[] { "雷电将军", "温迪", "枫原万叶" })] + //[InlineData(@"AutoFight\可识别异常场景\小三角能识别_出战2号位无法识别.png", new[] { "丝柯克", "爱可菲", "夜兰", "芙宁娜" })] + //[InlineData(@"AutoFight\可识别异常场景\小三角能识别_出战4号位无法识别.png", new[] { "丝柯克", "爱可菲", "夜兰", "芙宁娜" })] + //[InlineData(@"AutoFight\可识别异常场景\草露.png", new[] { "纳西妲", "空", "芙宁娜", "菈乌玛" })] + public void RecognisePartyAvatar_New_AvatarShouldBeRight(string screenshot1080P, string[]? expectedNames = null) + { + // + Mat mat = new Mat(@$"..\..\..\Assets\{screenshot1080P}"); + FakeSystemInfo systemInfo = new FakeSystemInfo(new Vanara.PInvoke.RECT(0, 0, mat.Width, mat.Height), 1); + // 桌面 -> 游戏捕获区域 -> 1080P区域 + var gameCaptureRegion = systemInfo.DesktopRectArea.Derive(mat, systemInfo.CaptureAreaRect.X, systemInfo.CaptureAreaRect.Y); + var imageRegion = gameCaptureRegion.DeriveTo1080P(); + + AutoFightConfig autoFightConfig = new AutoFightConfig(); + + FakeLogger logger = new FakeLogger(); + + // + var combatScenes = new CombatScenes(Predictor, new AutoFightAssets(systemInfo), logger, new ElementAssets(systemInfo), systemInfo).InitializeTeam(imageRegion, autoFightConfig); + + // + Assert.True(combatScenes.CheckTeamInitialized()); + if (expectedNames != null) + { + Assert.Equal(expectedNames.Length, combatScenes.AvatarCount); + for (var i = 0; i < expectedNames.Length; i++) + { + Assert.Equal(expectedNames[i], combatScenes.GetAvatars()[i].Name); + } + } + } + + /// + /// 测试普通的多人游戏下的角色头像识别 + /// + // [Theory] + // [InlineData(@"AutoFight\联机满编\别人进我世界_2人.png", new[] { "阿蕾奇诺", "钟离" })] + // public void WhatAvatarIsActive_New_AvatarShouldBeRight(string screenshot1080P) + // { + // // + // TaskContext.Instance().InitFakeForTest(); + // Mat mat = new Mat(@$"..\..\..\Assets\{screenshot1080P}"); + // var systemInfo = TaskContext.Instance().SystemInfo; + // // 桌面 -> 游戏捕获区域 -> 1080P区域 + // var gameCaptureRegion = systemInfo.DesktopRectArea.Derive(mat, systemInfo.CaptureAreaRect.X, systemInfo.CaptureAreaRect.Y); + // var imageRegion = gameCaptureRegion.DeriveTo1080P(); + // + // + // // + // var combatScenes = new CombatScenes().InitializeTeam(imageRegion); + // + // // + // Assert.True(combatScenes.CheckTeamInitialized()); + // + // } +} \ No newline at end of file diff --git a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/FakeSystemInfo.cs b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/FakeSystemInfo.cs index 209c2e33..ce498eba 100644 --- a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/FakeSystemInfo.cs +++ b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/FakeSystemInfo.cs @@ -15,6 +15,8 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests { public FakeSystemInfo(RECT gameScreenSize, double assetScale) { + DesktopRectArea = new(gameScreenSize.Width, gameScreenSize.Height); + GameScreenSize = gameScreenSize; // 0.28 改动,素材缩放比例不可以超过 1,也就是图像识别时分辨率大于 1920x1080 的情况下直接进行缩放 if (GameScreenSize.Width < 1920) @@ -36,7 +38,7 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests public double ScaleTo1080PRatio { get; } public RECT CaptureAreaRect { get ; set ; } - public Rect ScaleMax1080PCaptureRect { get ; set ; } + public Rect ScaleMax1080PCaptureRect { get; set; } = new Rect(0, 0, 1920, 1080); public Process GameProcess => throw new NotImplementedException(); @@ -44,6 +46,6 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests public int GameProcessId => throw new NotImplementedException(); - public DesktopRegion DesktopRectArea => throw new NotImplementedException(); + public DesktopRegion DesktopRectArea { get; set; } } } diff --git a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/GetGridIconsTests/GridIconsAccuracyTestTaskTests.cs b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/GetGridIconsTests/GridIconsAccuracyTestTaskTests.cs index 4d84245b..1f2dc175 100644 --- a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/GetGridIconsTests/GridIconsAccuracyTestTaskTests.cs +++ b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/GetGridIconsTests/GridIconsAccuracyTestTaskTests.cs @@ -43,19 +43,16 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.GetGridIconsTests using Mat mat = new Mat(@$"..\..\..\Assets\{screenshot}"); var gridItems = GridScreen.GridEnumerator.GetGridItems(mat, columns, findContoursAlpha: findContoursAlpha); - var rows = GridScreen.GridEnumerator.ClusterRows(gridItems, 10); + var cells = GridCell.ClusterToCells(gridItems, 10).OrderBy(c => c.RowNum).ThenBy(c => c.ColNum); // var result = new List<(string?, int)>(); - foreach (var row in rows) + foreach (var cell in cells) { - 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); - } + using Mat gridItemMat = mat.SubMat(cell.Rect); + using Mat icon = gridItemMat.GetGridIcon(); + var pred = GridIconsAccuracyTestTask.Infer(icon, this.session, this.prototypes); + result.Add(pred); } // diff --git a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/Model/GameUI/ArtifactSetFilterScreenTests.cs b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/Model/GameUI/ArtifactSetFilterScreenTests.cs index b6ff9b3a..1fa9dc30 100644 --- a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/Model/GameUI/ArtifactSetFilterScreenTests.cs +++ b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/Model/GameUI/ArtifactSetFilterScreenTests.cs @@ -13,6 +13,7 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.Model.GameUI [Theory] [InlineData(@"GameUI\ArtifactSetFilterBright.png", 20, 2)] [InlineData(@"GameUI\ArtifactSetFilterDark.png", 20, 2)] + [InlineData(@"GameUI\ArtifactSetFilterBlack.png", 20, 2)] // 只能识别到较少item(12个)的一个特例,用于验证Cell聚簇算法补齐效果 /// /// 测试获取圣遗物套装筛选界面中的项目,结果应正确 /// diff --git a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/Model/GameUI/GridScreenTests.cs b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/Model/GameUI/GridScreenTests.cs index 23cb235d..43128902 100644 --- a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/Model/GameUI/GridScreenTests.cs +++ b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/Model/GameUI/GridScreenTests.cs @@ -20,9 +20,11 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.Model.GameUI [Theory] [InlineData(@"AutoArtifactSalvage\ArtifactGrid.png", 4, 2)] + [InlineData(@"GetGridIcons\FoodGrid.png", 32, 8)] + [InlineData(@"GetGridIcons\WeaponGrid.png", 4, 2)] [InlineData(@"GetGridIcons\WeaponGrid3.png", 32, 8)] /// - /// 测试获取各种界面中的物品图标,结果应正确 + /// 测试获取各种界面中的物品图标,经过算法的后处理,结果应正确 /// public void GetGridIcons_ShouldBeRight(string screenshot, int count, int columns) { @@ -30,18 +32,20 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.Model.GameUI using Mat mat = new Mat(@$"..\..\..\Assets\{screenshot}"); // - var result = GridScreen.GridEnumerator.GetGridItems(mat, columns); + var rects = GridScreen.GridEnumerator.GetGridItems(mat, columns); + var cells = GridScreen.GridEnumerator.PostProcess(mat, rects, (int)(0.025 * mat.Height)); // - Assert.Equal(count, result.Count()); + Assert.Equal(count, cells.Count()); } [Theory] + [InlineData(@"AutoArtifactSalvage\ArtifactGrid.png", 4, 2)] [InlineData(@"GetGridIcons\FoodGrid.png", 32, 8)] [InlineData(@"GetGridIcons\WeaponGrid.png", 4, 2)] [InlineData(@"GetGridIcons\WeaponGrid3.png", 32, 8)] /// - /// 测试获取各种界面中的物品图标,使用复杂的cv算法,结果应正确 + /// 测试获取各种界面中的物品图标,使用复杂的cv算法,经过算法的后处理,结果应正确 /// public void GetGridIconsAlpha_ShouldBeRight(string screenshot, int count, int columns) { @@ -49,10 +53,11 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.Model.GameUI using Mat mat = new Mat(@$"..\..\..\Assets\{screenshot}"); // - var result = GridScreen.GridEnumerator.GetGridItems(mat, columns, findContoursAlpha: true); + var rects = GridScreen.GridEnumerator.GetGridItems(mat, columns, findContoursAlpha: true); + var cells = GridScreen.GridEnumerator.PostProcess(mat, rects, (int)(0.025 * mat.Height)); // - Assert.Equal(count, result.Count()); + Assert.Equal(count, cells.Count()); } [Fact] @@ -101,17 +106,14 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.Model.GameUI // var gridItems = GridScreen.GridEnumerator.GetGridItems(mat, columns, findContoursAlpha: true); - var rows = GridScreen.GridEnumerator.ClusterRows(gridItems, 10); + var cells = GridCell.ClusterToCells(gridItems, 10).OrderBy(c => c.RowNum).ThenBy(c => c.ColNum); var result = new List(); - foreach (var row in rows) + foreach (var cell in cells) { - foreach (Rect rect in row) - { - Mat gridItemMat = mat.SubMat(rect); - string numStr = gridItemMat.GetGridItemIconText(paddle.Get()); - result.Add(numStr); - } + using Mat gridItemMat = mat.SubMat(cell.Rect); + string numStr = gridItemMat.GetGridItemIconText(paddle.Get()); + result.Add(numStr); } //