using BetterGenshinImpact.Core.Config; using BetterGenshinImpact.Core.Recognition.OCR; using BetterGenshinImpact.GameTask.AutoFight.Assets; using BetterGenshinImpact.GameTask.AutoFight.Config; using BetterGenshinImpact.GameTask.AutoFight.Model; using BetterGenshinImpact.GameTask.Common; using BetterGenshinImpact.GameTask.Common.BgiVision; using BetterGenshinImpact.View.Drawable; using BetterGenshinImpact.GameTask.Model.Area; using OpenCvSharp; using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Vanara.PInvoke; using Point = System.Windows.Point; using Rect = OpenCvSharp.Rect; namespace BetterGenshinImpact.GameTask.SkillCd; /// /// 技能 CD 提示触发器 /// public class SkillCdTrigger : ITaskTrigger { public string Name => "SkillCd"; public bool IsEnabled { get => TaskContext.Instance().Config.SkillCdConfig.Enabled; set => TaskContext.Instance().Config.SkillCdConfig.Enabled = value; } public int Priority => 10; public bool IsExclusive => false; /// /// 在所有UI场景下都运行(包括大地图),确保遮罩层能处理消失 /// public GameUiCategory SupportedGameUiCategory => GameUiCategory.Unknown; private readonly double[] _cds = new double[4]; private readonly bool[] _prevKeys = new bool[4]; private bool _prevEKey = false; private DateTime _lastEKeyPress = DateTime.MinValue; private readonly DateTime[] _lastSetTime = new DateTime[4]; private string[] _teamAvatarNames = new string[4]; private Rect[] _teamIndexRects = new Rect[4]; private DateTime _lastTickTime = DateTime.Now; private DateTime _contextEnterTime = DateTime.MinValue; /// /// 离开场景时间,用于0.8秒防抖避免识别失误导致UI闪烁(仅影响UI渲染,不影响CD计时) /// private DateTime _contextLeaveTime = DateTime.MinValue; private bool _wasInContext = false; /// /// 上一次激活的角色索引(1-4),用于检测当前激活角色切换 /// private int _lastActiveIndex = -1; /// /// 上一次的队伍配置 /// private string[] _lastTeamAvatarNames = new string[4]; private int _lastSwitchFromSlot = -1; private DateTime _lastSwitchTime = DateTime.MinValue; private DateTime _lastPressIndexTime = DateTime.MinValue; // 换人按键时间 private volatile bool _isSyncingTeam = false; private DateTime _lastSyncTime = DateTime.MinValue; private ImageRegion? _lastImage = null; // 上一帧 private ImageRegion? _penultimateImage = null; // 上上帧(倒数第二帧) private readonly object _stateLock = new(); private readonly ILogger _logger = TaskControl.Logger; private readonly AvatarActiveCheckContext _activeCheckContext = new(); /// /// 初始化 /// public void Init() { // 清空帧缓存 _lastImage?.Dispose(); _lastImage = null; _penultimateImage?.Dispose(); _penultimateImage = null; for (int i = 0; i < 4; i++) { _cds[i] = 0; _prevKeys[i] = false; _teamAvatarNames[i] = string.Empty; _teamIndexRects[i] = default; _lastSetTime[i] = DateTime.MinValue; _lastTeamAvatarNames[i] = string.Empty; } _prevEKey = false; _lastEKeyPress = DateTime.MinValue; _wasInContext = false; _contextEnterTime = DateTime.MinValue; _contextLeaveTime = DateTime.MinValue; _lastTickTime = DateTime.Now; _lastActiveIndex = -1; _lastSwitchFromSlot = -1; _lastSwitchTime = DateTime.MinValue; _lastPressIndexTime = DateTime.MinValue; _lastSyncTime = DateTime.MinValue; if (!IsEnabled) { VisionContext.Instance().DrawContent.PutOrRemoveTextList("SkillCdText", null); } } /// /// 截图回调处理 /// public void OnCapture(CaptureContent content) { if (!IsEnabled) { VisionContext.Instance().DrawContent.PutOrRemoveTextList("SkillCdText", null); return; } var now = DateTime.Now; var delta = (now - _lastTickTime).TotalSeconds; _lastTickTime = now; // CD计时器持续运行 if (delta >= 0 && delta < 5) { for (int i = 0; i < 4; i++) { if (_cds[i] > 0) { _cds[i] -= delta; if (_cds[i] < 0) _cds[i] = 0; } } } // 场景检测(带0.5秒防抖,仅影响UI渲染) bool rawInContext = Bv.IsInMainUi(content.CaptureRectArea) || Bv.IsInDomain(content.CaptureRectArea); bool isInContext; if (rawInContext) { var multiGameStatus = PartyAvatarSideIndexHelper.DetectedMultiGameStatus(content.CaptureRectArea); if (multiGameStatus.IsInMultiGame) { // 检测到联机状态,自动关闭SkillCd IsEnabled = false; _logger.LogWarning("检测到联机状态,自动关闭冷却提示"); return; } _contextLeaveTime = DateTime.MinValue; isInContext = true; } else { if (_wasInContext && _contextLeaveTime == DateTime.MinValue) { _contextLeaveTime = now; } // 离开后0.8秒内仍视为在场景中,防止识别失误 isInContext = _contextLeaveTime != DateTime.MinValue && (now - _contextLeaveTime).TotalSeconds < 0.8; } // 离开场景时隐藏UI,但保留角色信息和CD数据 if (!isInContext) { if (_wasInContext) { VisionContext.Instance().DrawContent.PutOrRemoveTextList("SkillCdText", null); _wasInContext = false; _contextEnterTime = DateTime.MinValue; _lastActiveIndex = -1; } _lastImage?.Dispose(); _lastImage = null; _penultimateImage?.Dispose(); _penultimateImage = null; return; } if (!_wasInContext) { // 进入场景时同步队伍信息并检测队伍变化 _contextEnterTime = now; _lastSyncTime = DateTime.MinValue; _wasInContext = true; _isSyncingTeam = true; Task.Run(async () => { // 确保画面加载完成,提高识别成功率 await Task.Delay(500); var delaySinceLastPressIndex = (DateTime.Now - _lastPressIndexTime).TotalSeconds; if (delaySinceLastPressIndex < 1.1) { // 刚按过换人键,人物头像还在读秒,此时yolo识别可能会失败 await Task.Delay(TimeSpan.FromSeconds(1.1 - delaySinceLastPressIndex)); } CombatScenes? scenes = null; try { scenes = RunnerContext.Instance.TrySyncCombatScenesSilent(); if (scenes != null && scenes.CheckTeamInitialized()) { var avatars = scenes.GetAvatars(); if (avatars.Count >= 1) { var newTeamNames = avatars.Select(a => a.Name).ToArray(); // 检测队伍配置是否变化 bool teamChanged = false; for (int i = 0; i < 4; i++) { string newName = i < newTeamNames.Length ? newTeamNames[i] : string.Empty; if (_lastTeamAvatarNames[i] != newName) { teamChanged = true; break; } } lock (_stateLock) { if (teamChanged) { bool wasFullTeam = _lastTeamAvatarNames.All(n => !string.IsNullOrEmpty(n)); bool isNowFullTeam = newTeamNames.Length == 4; bool isFullTeam = wasFullTeam && isNowFullTeam; if (isFullTeam) { _logger.LogInformation("[SkillCD] 队伍配置变化: {OldTeam} -> {NewTeam}", string.Join(",", _lastTeamAvatarNames), string.Join(",", newTeamNames)); } for (int i = 0; i < 4; i++) { _cds[i] = 0; _lastSetTime[i] = DateTime.MinValue; } _lastActiveIndex = -1; } SyncAvatarInfo(avatars.ToList()); for (int i = 0; i < 4; i++) { _lastTeamAvatarNames[i] = i < newTeamNames.Length ? newTeamNames[i] : string.Empty; } } } else { lock (_stateLock) { // 同步失败/无人时清空UI,但保留数据 for (int i = 0; i < 4; i++) { _teamAvatarNames[i] = string.Empty; _teamIndexRects[i] = default; } } } } else { lock (_stateLock) { for (int i = 0; i < 4; i++) { _teamAvatarNames[i] = string.Empty; _teamIndexRects[i] = default; } } } } finally { scenes?.Dispose(); lock (_stateLock) { _isSyncingTeam = false; // 无论成功失败,同步结束,允许渲染 } } }); } // 场景切入缓冲期,等待UI稳定 if ((now - _contextEnterTime).TotalSeconds < 0.5) { return; } // 监听元素战技 (E) 键物理输入 var elementalSkillKey = (int)TaskContext.Instance() .Config.KeyBindingsConfig.ElementalSkill.ToVK(); short eKeyState = User32.GetAsyncKeyState(elementalSkillKey); bool isEDown = (eKeyState & 0x8000) != 0; if (isEDown && !_prevEKey) _lastEKeyPress = now; _prevEKey = isEDown; // 监听换人操作 (数字键 1-4) int pressedIndex = -1; for (int i = 0; i < 4; i++) { short keyState = User32.GetAsyncKeyState((int)(User32.VK.VK_1 + (byte)i)); bool isDown = (keyState & 0x8000) != 0; if (isDown && !_prevKeys[i]) pressedIndex = i; _prevKeys[i] = isDown; _lastPressIndexTime = DateTime.Now; } if (_lastImage != null) { if (pressedIndex != -1) { ImageRegion frameToUse = _penultimateImage ?? _lastImage; if (frameToUse != null) { HandleActionTrigger(frameToUse, pressedIndex); } } if (_prevEKey && TaskContext.Instance().Config.SkillCdConfig.TriggerOnSkillUse) { ImageRegion frameToUse = _penultimateImage ?? _lastImage; if (frameToUse != null) { HandleActionTrigger(frameToUse, pressedIndex); } } } // 更新帧缓存队列 _penultimateImage?.Dispose(); _penultimateImage = _lastImage; // 把上一帧移到倒数第二帧 // 记录当前帧为上一帧(深拷贝,避免current用完会被dispose) _lastImage = new ImageRegion( content.CaptureRectArea.SrcMat.Clone(), content.CaptureRectArea.X, content.CaptureRectArea.Y ); UpdateOverlay(); } /// /// 同步角色基础数据 /// private void SyncAvatarInfo(List avatars) { for (int i = 0; i < 4; i++) { if (i < avatars.Count) { _teamAvatarNames[i] = avatars[i].Name; _teamIndexRects[i] = avatars[i].IndexRect; } else { _teamAvatarNames[i] = string.Empty; _teamIndexRects[i] = default; } } } /// /// 处理按键切换角色时的CD记录 /// private void HandleActionTrigger(ImageRegion frame, int pressedTarget) { int activeIdx = IdentifyActiveIndex(frame, new AvatarActiveCheckContext()); if (activeIdx <= 0) return; int slot = activeIdx - 1; // 记录被切走角色的CD if (slot != pressedTarget) { double ocrVal = RecognizeSkillCd(frame); if (ocrVal > 0) { _cds[slot] = ocrVal; _lastSetTime[slot] = DateTime.Now; // 记录切人保护 _lastSwitchFromSlot = slot; _lastSwitchTime = DateTime.Now; } else { // OCR识别失败,尝试兜底 bool justUsedE = (DateTime.Now - _lastEKeyPress).TotalSeconds < 1.1; bool isVisualReady = Bv.IsSkillReady(frame, activeIdx, false); if (isVisualReady) { if (justUsedE) { ApplyFallbackCd(slot); } else if (_cds[slot] > 0) { // 保留原CD } else { _cds[slot] = 0; } } else { if (justUsedE) { ApplyFallbackCd(slot); } } } } // 更新当前激活角色索引(不清零CD,让计时器持续运行) _lastActiveIndex = pressedTarget + 1; } /// /// 检测当前激活角色并同步技能状态 /// private void CheckAndSyncActiveStatus(ImageRegion frame) { int activeIdx = IdentifyActiveIndex(frame, _activeCheckContext); if (activeIdx > 0) { // int slot = activeIdx - 1; // // // 更新当前激活角色索引(切换角色不清零CD) // if (_lastActiveIndex != activeIdx) // { // _lastActiveIndex = activeIdx; // } // // // 检测技能是否就绪,就绪则归零 // // 额外保护:处于切人冷却期时不检测 // bool isInSwitchProtect = (slot == _lastSwitchFromSlot) && (DateTime.Now - _lastSwitchTime).TotalSeconds < 1.0; // // if (activeIdx == slot + 1 && !isInSwitchProtect) // { // bool isReady = Bv.IsSkillReady(frame, activeIdx, false); // if (isReady) // { // // 默认逻辑:识别到技能就绪时,不清零当前计时 // // 防止因开大招全屏遮挡导致误判为Ready从而错误清零计数器 // // 让倒计时自然跑完 // } // } _lastActiveIndex = activeIdx; } } /// /// 获取自定义规则中的CD值 /// 返回值: /// - double值:命中规则,应强制设定为该值 /// - null:未命中规则,走默认逻辑 /// private double? GetCustomCdRule(string name) { if (string.IsNullOrEmpty(name)) return null; var config = ParseCustomCdConfig(); if (config.TryGetValue(name, out var val)) { // 如果用户只写了名字没写数值,尝试读默认配置 if (!val.HasValue) { if (DefaultAutoFightConfig.CombatAvatarMap.TryGetValue(name, out var info)) { return info.SkillCd; } return 0; // 名字匹配但无默认配置,视为0 } return val.Value; } return null; } /// /// 应用角色的冷却时间 /// private void ApplyFallbackCd(int slot) { var name = _teamAvatarNames[slot]; // 1. 优先自定义规则 double? customRule = GetCustomCdRule(name); if (customRule.HasValue) { _cds[slot] = customRule.Value; _lastSetTime[slot] = DateTime.Now; return; } // 2. 默认兜底 if (!string.IsNullOrEmpty(name) && DefaultAutoFightConfig.CombatAvatarMap.TryGetValue(name, out var info)) { _cds[slot] = info.SkillCd; _lastSetTime[slot] = DateTime.Now; } else { _cds[slot] = 0; } } private Dictionary ParseCustomCdConfig() { var result = new Dictionary(); var list = TaskContext.Instance().Config.SkillCdConfig.CustomCdList; if (list == null) return result; foreach (var item in list) { if (!string.IsNullOrWhiteSpace(item.RoleName)) { if (!result.ContainsKey(item.RoleName)) { result[item.RoleName] = item.CdValue; } } } return result; } private int IdentifyActiveIndex(ImageRegion region, AvatarActiveCheckContext context) { var validRects = _teamIndexRects.Any(r => r != default) ? _teamIndexRects.Where(r => r != default).ToArray() : AutoFightAssets.Instance.AvatarIndexRectList.ToArray(); return PartyAvatarSideIndexHelper.GetAvatarIndexIsActiveWithContext(region, validRects, context); } private double RecognizeSkillCd(ImageRegion image) { try { var eCdRect = AutoFightAssets.Instance.ECooldownRect; using var crop = image.DeriveCrop(eCdRect); var roi = crop.SrcMat; using var whiteMask = new Mat(); Cv2.InRange(roi, new Scalar(230, 230, 230), new Scalar(255, 255, 255), whiteMask); var text = OcrFactory.Paddle.OcrWithoutDetector(whiteMask); if (string.IsNullOrWhiteSpace(text)) return 0; var match = Regex.Match(text, @"\d+(\.\d+)?"); if (match.Success && double.TryParse(match.Value, out var val)) { // 减去两帧的时间作为补偿 int intervalMs = TaskContext.Instance().Config.TriggerInterval; double compensation = (intervalMs * 2) / 1000.0; val -= compensation; return (val > 0 && val < 60) ? val : 0; } } catch (Exception ex) { _logger.LogDebug(ex, "[SkillCD] OCR识别CD失败"); } return 0; } /// /// 更新 UI 层渲染 /// private void UpdateOverlay() { var drawContent = VisionContext.Instance().DrawContent; var sideRects = AutoFightAssets.Instance.AvatarSideIconRectList; var config = TaskContext.Instance().Config.SkillCdConfig; if (sideRects == null || sideRects.Count < 4) { drawContent.PutOrRemoveTextList("SkillCdText", null); return; } var systemInfo = TaskContext.Instance().SystemInfo; double factor = (double)systemInfo.GameScreenSize.Width / systemInfo.ScaleMax1080PCaptureRect.Width; // 使用配置中的坐标(保留一位小数) double userPX = Math.Round(config.PX, 1); double userPY = Math.Round(config.PY, 1); double userGap = Math.Round(config.Gap, 1); double basePx = userPX * factor; double basePy = userPY * factor; double intervalY = userGap * factor; var textList = new List(); if (_isSyncingTeam) { drawContent.PutOrRemoveTextList("SkillCdText", null); return; } // 检查是否有足够的角色信息(必须恰好4人) int validAvatarCount = _teamAvatarNames.Count(n => !string.IsNullOrEmpty(n)); // _logger.LogDebug("[SkillCD] UpdateOverlay: 有效角色数量={Count}, Names={Names}", validAvatarCount, string.Join(",", _teamAvatarNames)); if (validAvatarCount != 4) { // 不是4人,确保清空 if (drawContent.TextList.ContainsKey("SkillCdText")) { drawContent.PutOrRemoveTextList("SkillCdText", null); } return; } for (int i = 0; i < 4; i++) { if (!string.IsNullOrEmpty(_teamAvatarNames[i])) { // 如果启用了"冷却为0时隐藏",且CD为0,则跳过 if (config.HideWhenZero && _cds[i] <= 0) { continue; } var px = basePx; var py = basePy + intervalY * i; textList.Add(new TextDrawable(_cds[i].ToString("F1"), new Point(px, py))); } } if (textList.Count == 0) drawContent.PutOrRemoveTextList("SkillCdText", null); else drawContent.PutOrRemoveTextList("SkillCdText", textList); } }