using BetterGenshinImpact.Core.Config; using BetterGenshinImpact.Core.Recognition; using BetterGenshinImpact.Core.Recognition.OCR; using BetterGenshinImpact.Core.Recognition.ONNX.SVTR; using BetterGenshinImpact.Core.Script.Dependence.Model.TimerConfig; using BetterGenshinImpact.Core.Simulator; using BetterGenshinImpact.GameTask.AutoPick.Assets; using BetterGenshinImpact.Helpers; using BetterGenshinImpact.Service; using Microsoft.Extensions.Logging; using OpenCvSharp; using System; using System.Collections.Generic; using System.Diagnostics; using System.Text.Json; using System.Text.RegularExpressions; using System.Threading; using BetterGenshinImpact.GameTask.Model.Area; namespace BetterGenshinImpact.GameTask.AutoPick; public partial class AutoPickTrigger : ITaskTrigger { private readonly ILogger _logger = App.GetLogger(); private readonly ITextInference _pickTextInference = TextInferenceFactory.Pick; public string Name => "自动拾取"; public bool IsEnabled { get; set; } public int Priority => 30; public bool IsExclusive => false; private readonly AutoPickAssets _autoPickAssets; /// /// 拾取黑名单 /// private HashSet _blackList = []; /// /// 拾取白名单 /// private HashSet _whiteList = []; private RecognitionObject _pickRo; // 外部配置 private AutoPickExternalConfig? _externalConfig; public AutoPickTrigger() { _autoPickAssets = AutoPickAssets.Instance; _pickRo = _autoPickAssets.PickRo; } public AutoPickTrigger(AutoPickExternalConfig? config) : this() { _externalConfig = config; } public void Init() { var config = TaskContext.Instance().Config.AutoPickConfig; IsEnabled = config.Enabled; if (config.BlackListEnabled) { _blackList = ReadJson(@"Assets\Config\Pick\default_pick_black_lists.json"); var userBlackList = ReadText(@"User\pick_black_lists.txt"); if (userBlackList.Count > 0) { _blackList.UnionWith(userBlackList); } } if (config.WhiteListEnabled) { _whiteList = ReadText(@"User\pick_white_lists.txt"); } } private HashSet ReadJson(string jsonFilePath) { try { var json = Global.ReadAllTextIfExist(jsonFilePath); if (!string.IsNullOrEmpty(json)) { return JsonSerializer.Deserialize>(json, ConfigService.JsonOptions) ?? []; } } catch (Exception e) { _logger.LogError(e, "读取拾取黑/白名单失败"); MessageBox.Error("读取拾取黑/白名单失败,请确认修改后的拾取黑/白名单内容格式是否正确!"); } return []; } private HashSet ReadText(string textFilePath) { try { var txt = Global.ReadAllTextIfExist(textFilePath); if (!string.IsNullOrEmpty(txt)) { // 明确指定使用 char[] 重载版本 return new HashSet(txt.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)); } } catch (Exception e) { _logger.LogError(e, "读取拾取黑/白名单失败"); MessageBox.Error("读取拾取黑/白名单失败,请确认修改后的拾取黑/白名单内容格式是否正确!"); } return []; } /// /// 用于日志只输出一次 /// private string _lastText = string.Empty; /// /// 用于日志只输出一次 /// private int _prevClickFrameIndex = -1; //private int _fastModePickCount = 0; public void OnCapture(CaptureContent content) { while (RunnerContext.Instance.AutoPickTriggerStopCount > 0) { Thread.Sleep(1000); } var speedTimer = new SpeedTimer(); using var foundRectArea = content.CaptureRectArea.Find(_pickRo); if (foundRectArea.IsEmpty()) { // 没有识别到F键,先判断是否有滚轮图标信息 if (HasScrollIcon(content.CaptureRectArea)) { // 滚轮下 Simulation.SendInput.Mouse.VerticalScroll(2); Thread.Sleep(50); } return; } speedTimer.Record($"识别到拾取键"); if (_externalConfig is { ForceInteraction: true }) { LogPick(content, "直接拾取"); Simulation.SendInput.Keyboard.KeyPress(AutoPickAssets.Instance.PickVk); return; } var scale = TaskContext.Instance().SystemInfo.AssetScale; var config = TaskContext.Instance().Config.AutoPickConfig; // 识别到拾取键,开始识别物品图标 var isExcludeIcon = false; _autoPickAssets.ChatIconRo.RegionOfInterest = new Rect( foundRectArea.X + (int)(config.ItemIconLeftOffset * scale), foundRectArea.Y, (int)((config.ItemTextLeftOffset - config.ItemIconLeftOffset) * scale), foundRectArea.Height); var chatIconRa = content.CaptureRectArea.Find(_autoPickAssets.ChatIconRo); speedTimer.Record("识别聊天图标"); if (!chatIconRa.IsEmpty()) { // 物品图标是聊天气泡,一般是NPC对话,文字不在白名单不拾取 isExcludeIcon = true; } else { _autoPickAssets.SettingsIconRo.RegionOfInterest = _autoPickAssets.ChatIconRo.RegionOfInterest; var settingsIconRa = content.CaptureRectArea.Find(_autoPickAssets.SettingsIconRo); speedTimer.Record("识别设置图标"); if (!settingsIconRa.IsEmpty()) { // 物品图标是设置图标,一般是解谜、活动、电梯等 isExcludeIcon = true; } } if (!config.WhiteListEnabled && isExcludeIcon) { // 默认不拾取且没有白名单直接放弃OCR return; } if (!config.WhiteListEnabled && !config.BlackListEnabled && !isExcludeIcon) { // 没有黑白名单直接拾取 Simulation.SendInput.Keyboard.KeyPress(AutoPickAssets.Instance.PickVk); LogPick(content, "黑名单未启用,直接拾取"); } //if (config.FastModeEnabled && !isExcludeIcon) //{ // _fastModePickCount++; // if (_fastModePickCount > 2) // { // _fastModePickCount = 0; // LogPick(content, "急速拾取"); // Simulation.SendInput.Keyboard.KeyPress(VirtualKeyCode.VK_F); // } // return; //} // 这类文字识别比较特殊,都是针对某个场景的文字识别,所以暂时未抽象到识别对象中 // 计算出文字区域 var textRect = new Rect(foundRectArea.X + (int)(config.ItemTextLeftOffset * scale), foundRectArea.Y, (int)((config.ItemTextRightOffset - config.ItemTextLeftOffset) * scale), foundRectArea.Height); if (textRect.X + textRect.Width > content.CaptureRectArea.CacheGreyMat.Width || textRect.Y + textRect.Height > content.CaptureRectArea.CacheGreyMat.Height) { Debug.WriteLine("AutoPickTrigger: 文字区域 out of range"); return; } // var textMat = new Mat(content.CaptureRectArea.SrcGreyMat, textRect); var gradMat = new Mat(content.CaptureRectArea.CacheGreyMat, new Rect(textRect.X, textRect.Y, textRect.Width, Math.Min(textRect.Height, 3))); var avgGrad = gradMat.Sobel(MatType.CV_32F, 1, 0).Mean().Val0; if (avgGrad < -3) { Debug.WriteLine($"AutoPickTrigger: 已在拾取中,跳过本次拾取 {avgGrad}"); return; } string text; if (config.OcrEngine == nameof(PickOcrEngineEnum.Yap)) { var textMat = new Mat(content.CaptureRectArea.CacheGreyMat, textRect); text = _pickTextInference.Inference(textMat); } else { var textMat = new Mat(content.CaptureRectArea.SrcMat, textRect); var boundingRect = GetWhiteTextBoundingRect(textMat); // 如果找到有效区域 if (boundingRect.Width > 5 && boundingRect.Height > 5) { // 截取只包含文字的区域 var textOnlyMat = new Mat(textMat, new Rect(0, 0, boundingRect.Right + 3 < textMat.Width ? boundingRect.Right + 3 : textMat.Width, textMat.Height)); text = OcrFactory.Paddle.OcrWithoutDetector(textOnlyMat); } else { text = OcrFactory.Paddle.Ocr(textMat); } } speedTimer.Record("文字识别"); if (!string.IsNullOrEmpty(text)) { // 唯一一个动态拾取项,特殊处理,不拾取 if (text.Contains("长时间")) { return; } // 纳塔部落中文名特殊处理,不拾取 if (text.Contains("我在") && (text.Contains("声望") || text.Contains("回声") || text.Contains("悬木人") || text.Contains("流泉"))) { return; } // 单个字符不拾取 var simpleText = PunctuationAndSpacesRegex().Replace(text, ""); if (simpleText.Length <= 1) { return; } // 纯英文不拾取 if (StringUtils.IsPureEnglish(text)) { return; } if (config.WhiteListEnabled && (_whiteList.Contains(text) || _whiteList.Contains(simpleText))) { LogPick(content, text); Simulation.SendInput.Keyboard.KeyPress(AutoPickAssets.Instance.PickVk); return; } speedTimer.Record("白名单判断"); if (isExcludeIcon) { //Debug.WriteLine("AutoPickTrigger: 物品图标是聊天气泡,一般是NPC对话,不拾取"); return; } if (config.BlackListEnabled && (_blackList.Contains(text) || _blackList.Contains(simpleText))) { return; } speedTimer.Record("黑名单判断"); LogPick(content, text); Simulation.SendInput.Keyboard.KeyPress(AutoPickAssets.Instance.PickVk); } speedTimer.DebugPrint(); } public static Rect GetWhiteTextBoundingRect(Mat textMat) { // 预处理提取纯白色文字 var processedMat = new Mat(); // 提取白色文字 (255,255,255) Cv2.InRange(textMat, new Scalar(254, 254, 254), new Scalar(255, 255, 255), processedMat); // 形态学操作,先腐蚀后膨胀,去除噪点并保持文字完整 var kernel = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(2, 2)); Cv2.MorphologyEx(processedMat, processedMat, MorphTypes.Open, kernel, iterations: 1); Cv2.Dilate(processedMat, processedMat, kernel, iterations: 1); // 寻找非零区域,即文字区域 Rect boundingRect = Cv2.BoundingRect(processedMat); return boundingRect; } private bool HasScrollIcon(ImageRegion captureRectArea) { // 固定区域颜色判断 // (1062,537) (255,233,44) 黄色 // (1062,524) (255,255,255) 白色 // (1062,583) (255,255,255) 白色 var mat = captureRectArea.SrcMat; var color1 = mat.At(537, 1062); var color2 = mat.At(524, 1062); var color3 = mat.At(554, 1062); // BGR 的格式 if (color1.Item2 == 255 && color1.Item1 == 233 && color1.Item0 == 44 && color2.Item2 == 255 && color2.Item1 == 255 && color2.Item0 == 255 && color3.Item2 == 255 && color3.Item1 == 255 && color3.Item0 == 255) { return true; } return false; } /// /// 相同文字前后3帧内只输出一次 /// /// /// private void LogPick(CaptureContent content, string text) { if (_lastText != text || (_lastText == text && Math.Abs(content.FrameIndex - _prevClickFrameIndex) >= 5)) { _logger.LogInformation("交互或拾取:{Text}", text); } _lastText = text; _prevClickFrameIndex = content.FrameIndex; } [GeneratedRegex(@"^[\p{P} ]+|[\p{P} ]+$")] private static partial Regex PunctuationAndSpacesRegex(); }