using BetterGenshinImpact.Core.Config; using BetterGenshinImpact.Core.Recognition.OCR; using BetterGenshinImpact.Core.Recognition.ONNX; using BetterGenshinImpact.Core.Simulator; using BetterGenshinImpact.Core.Simulator.Extensions; using BetterGenshinImpact.GameTask.AutoFight.Assets; using BetterGenshinImpact.GameTask.AutoFight.Model; using BetterGenshinImpact.GameTask.AutoFight.Script; using BetterGenshinImpact.GameTask.AutoGeniusInvokation.Exception; using BetterGenshinImpact.GameTask.AutoPick.Assets; using BetterGenshinImpact.GameTask.Common.Map; using BetterGenshinImpact.GameTask.Model.Area; using BetterGenshinImpact.Helpers; using BetterGenshinImpact.Service.Notification; using BetterGenshinImpact.View.Drawable; using Microsoft.Extensions.Logging; using OpenCvSharp; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; using BetterGenshinImpact.Core.Recognition; using BetterGenshinImpact.GameTask.AutoTrackPath; using BetterGenshinImpact.GameTask.Common.BgiVision; using BetterGenshinImpact.GameTask.Common.Element.Assets; using BetterGenshinImpact.GameTask.Common.Job; using BetterGenshinImpact.Service.Notification.Model.Enum; using static BetterGenshinImpact.GameTask.Common.TaskControl; using static Vanara.PInvoke.Kernel32; using static Vanara.PInvoke.User32; using Microsoft.Extensions.Localization; using System.Globalization; using System.Text.RegularExpressions; using BetterGenshinImpact.GameTask.AutoArtifactSalvage; using System.Collections.ObjectModel; using BetterGenshinImpact.Core.Script.Dependence; using BetterGenshinImpact.GameTask.AutoDomain.Model; using BetterGenshinImpact.GameTask.Common; using Compunet.YoloSharp; using Microsoft.Extensions.DependencyInjection; using BetterGenshinImpact.GameTask.AutoFight; namespace BetterGenshinImpact.GameTask.AutoDomain; public class AutoDomainTask : ISoloTask { public string Name => "自动秘境"; private readonly AutoDomainParam _taskParam; private readonly BgiYoloPredictor _predictor; private readonly AutoDomainConfig _config; private readonly CombatScriptBag _combatScriptBag; private CancellationToken _ct; private ObservableCollection ConfigList = []; private readonly string challengeCompletedLocalizedString; private readonly string autoLeavingLocalizedString; private readonly string skipLocalizedString; private readonly string leyLineDisorderLocalizedString; private readonly string clickanywheretocloseLocalizedString; private readonly string matchingChallengeString; private readonly string rapidformationString; private readonly string limitedFullyString; private readonly string limitedFullyAllString; private List _resinPriorityListWhenSpecifyUse; public AutoDomainTask(AutoDomainParam taskParam) { AutoFightAssets.DestroyInstance(); _taskParam = taskParam; _predictor = App.ServiceProvider.GetRequiredService().CreateYoloPredictor(BgiOnnxModel.BgiTree); _config = TaskContext.Instance().Config.AutoDomainConfig; _combatScriptBag = CombatScriptParser.ReadAndParse(_taskParam.CombatStrategyPath); _resinPriorityListWhenSpecifyUse = ResinUseRecord.BuildFromDomainParam(taskParam); IStringLocalizer stringLocalizer = App.GetService>() ?? throw new NullReferenceException(); CultureInfo cultureInfo = new CultureInfo(TaskContext.Instance().Config.OtherConfig.GameCultureInfoName); this.challengeCompletedLocalizedString = stringLocalizer.WithCultureGet(cultureInfo, "挑战达成"); this.autoLeavingLocalizedString = stringLocalizer.WithCultureGet(cultureInfo, "自动退出"); this.skipLocalizedString = stringLocalizer.WithCultureGet(cultureInfo, "跳过"); this.leyLineDisorderLocalizedString = stringLocalizer.WithCultureGet(cultureInfo, "地脉异常"); this.clickanywheretocloseLocalizedString = stringLocalizer.WithCultureGet(cultureInfo, "点击任意位置关闭"); this.matchingChallengeString = stringLocalizer.WithCultureGet(cultureInfo, "匹配挑战"); this.rapidformationString = stringLocalizer.WithCultureGet(cultureInfo, "快速编队"); this.limitedFullyString = stringLocalizer.WithCultureGet(cultureInfo, "限时全开"); this.limitedFullyAllString = stringLocalizer.WithCultureGet(cultureInfo, "限时开放"); } private static RecognitionObject GetConfirmRa(params string[] targetText) { var screenArea = CaptureToRectArea(); var x = (int)(screenArea.Width * 0.5); var y = (int)(screenArea.Height * 0.5); var width = (int)(screenArea.Width * 0.5); var height = (int)(screenArea.Height * 0.5); return RecognitionObject.OcrMatch(x, y, width, height, targetText); } public async Task Start(CancellationToken ct) { _ct = ct; Init(); Notify.Event(NotificationEvent.DomainStart).Success("自动秘境启动"); // 复活重试 for (var i = 0; i < _config.ReviveRetryCount; i++) { try { await DoDomain(); // 其他场景不重试 break; } catch (RetryException e) { // 只有选择了秘境的时候才会重试 if (!string.IsNullOrEmpty(_taskParam.DomainName)) { var msg = e.Message; if (msg.Contains("复活")) { msg = "存在角色死亡,复活后重试秘境..."; } Logger.LogWarning("自动秘境:{Text}", msg); await Delay(2000, ct); Notify.Event(NotificationEvent.DomainRetry).Error(msg); continue; } throw; } } await Delay(2000, ct); await Bv.WaitForMainUi(_ct, 30); await Delay(2000, ct); await ArtifactSalvage(); Notify.Event(NotificationEvent.DomainEnd).Success("自动秘境结束"); } private async Task DoDomain() { // 传送到秘境 await TpDomain(); // 切换队伍 // await SwitchParty(_taskParam.PartyName); // 前置进入秘境 await EnterDomain(); var combatScenes = new CombatScenes(); for (var i = 0; i < _taskParam.DomainRoundNum; i++) { // 0. 关闭秘境提示 Logger.LogDebug("0. 关闭秘境提示"); await CloseDomainTip(); //0.5. 初始化队伍,只执行一次 if (i == 0) { combatScenes = new CombatScenes().InitializeTeam(CaptureToRectArea()); } RetryTeamInit(combatScenes); // 队伍没初始化成功则重试 // 0. 切换到第一个角色 var combatCommands = FindCombatScriptAndSwitchAvatar(combatScenes); // 1. 走到钥匙处启动 Logger.LogInformation("自动秘境:{Text}", "1. 走到钥匙处启动"); await WalkToPressF(); // 2. 执行战斗(战斗线程、视角线程、检测战斗完成线程) Logger.LogInformation("自动秘境:{Text}", "2. 执行战斗策略"); await StartFight(combatScenes, combatCommands); combatScenes.AfterTask(); EndFightWait(); // 3. 寻找石化古树 并左右移动直到石化古树位于屏幕中心 Logger.LogInformation("自动秘境:{Text}", "3. 寻找石化古树"); await FindPetrifiedTree(); // 4. 走到石化古树处 Logger.LogInformation("自动秘境:{Text}", "4. 走到石化古树处"); await WalkToPressF(); // 5. 快速领取奖励并判断是否有下一轮 Logger.LogInformation("自动秘境:{Text}", "5. 领取奖励"); if (!await GettingTreasure()) { Logger.LogInformation("体力耗尽或者设置轮次已达标,结束自动秘境"); break; } Notify.Event(NotificationEvent.DomainReward).Success("自动秘境奖励领取"); } } private void Init() { LogScreenResolution(); if (_config.AutoEat) { TaskTriggerDispatcher.Instance().AddTrigger("AutoEat", null); } if (_config.SpecifyResinUse) { Logger.LogInformation("→ {Text} 指定使用树脂", "自动秘境,"); } else { Logger.LogInformation("→ {Text} 用尽所有浓缩树脂和原粹树脂后结束", "自动秘境,"); } } private void LogScreenResolution() { var gameScreenSize = SystemControl.GetGameScreenRect(TaskContext.Instance().GameHandle); if (gameScreenSize.Width * 9 != gameScreenSize.Height * 16) { Logger.LogError("游戏窗口分辨率不是 16:9 !当前分辨率为 {Width}x{Height} , 非 16:9 分辨率的游戏无法正常使用自动秘境功能 !", gameScreenSize.Width, gameScreenSize.Height); throw new Exception("游戏窗口分辨率不是 16:9"); } if (gameScreenSize.Width < 1920 || gameScreenSize.Height < 1080) { Logger.LogWarning("游戏窗口分辨率小于 1920x1080 !当前分辨率为 {Width}x{Height} , 小于 1920x1080 的分辨率的游戏可能无法正常使用自动秘境功能 !", gameScreenSize.Width, gameScreenSize.Height); } } private void RetryTeamInit(CombatScenes combatScenes) { if (!combatScenes.CheckTeamInitialized()) { combatScenes.InitializeTeam(CaptureToRectArea()); if (!combatScenes.CheckTeamInitialized()) { throw new Exception("识别队伍角色失败,请在较暗背景下重试,比如游戏时间调整成夜晚。或者直接使用强制指定当前队伍角色的功能。"); } } } private async Task TpDomain() { // 传送到秘境 if (!string.IsNullOrEmpty(_taskParam.DomainName)) { if (MapLazyAssets.Instance.DomainPositionMap.TryGetValue(_taskParam.DomainName, out var domainPosition)) { Logger.LogInformation("自动秘境:传送到秘境{Text}", _taskParam.DomainName); await new TpTask(_ct).Tp(domainPosition.X, domainPosition.Y); await Delay(1000, _ct); await Bv.WaitForMainUi(_ct); var menuFound = false; if ("芬德尼尔之顶".Equals(_taskParam.DomainName)) { menuFound = await NewRetry.WaitForElementAppear( 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)) { Simulation.SendInput.SimulateAction(GIActions.MoveForward, KeyType.KeyDown); Thread.Sleep(500); Simulation.SendInput.SimulateAction(GIActions.MoveForward, KeyType.KeyUp); menuFound = await NewRetry.WaitForElementAppear( AutoPickAssets.Instance.PickRo, () => Simulation.SendInput.SimulateAction(GIActions.MoveLeft, KeyType.KeyDown), _ct, 20, 500 ); Simulation.SendInput.SimulateAction(GIActions.MoveLeft, KeyType.KeyUp); } else if ("太山府".Equals(_taskParam.DomainName)) { menuFound = await NewRetry.WaitForElementAppear( AutoPickAssets.Instance.PickRo, () => { }, _ct, 20, 500 ); } else { menuFound = await NewRetry.WaitForElementAppear( AutoPickAssets.Instance.PickRo, () => Simulation.SendInput.SimulateAction(GIActions.MoveForward, KeyType.KeyDown), _ct, 20, 500 ); Simulation.SendInput.SimulateAction(GIActions.MoveForward, KeyType.KeyUp); } if (!menuFound) { throw new Exception("请检查是否在秘境门前"); } var menu = await NewRetry.WaitForElementAppear( GetConfirmRa("单人挑战"), () => Simulation.SendInput.Keyboard.KeyPress(AutoPickAssets.Instance.PickVk), _ct, 20, 500 ); if (!menu) { throw new Exception("请检查是否已进入秘境页面"); } } else { Logger.LogError("自动秘境:未找到对应的秘境{Text}的传送点", _taskParam.DomainName); throw new Exception($"未找到对应的秘境{_taskParam.DomainName}的传送点"); } } } /// /// 切换队伍 /// /// /// private async Task SwitchParty(string? partyName) { if (!string.IsNullOrEmpty(partyName)) { var b = await new SwitchPartyTask().Start(partyName, _ct); await Delay(500, _ct); return b; } return true; } private async Task EnterDomain() { var fightAssets = AutoFightAssets.Instance; var menuFound = await NewRetry.WaitForElementAppear( GetConfirmRa("单人挑战"), () => Simulation.SendInput.Keyboard.KeyPress(AutoPickAssets.Instance.PickVk), _ct, 10, 1000 ); if (!menuFound) { Logger.LogWarning("单人挑战 按键未出现,请检查是否已进入秘境页面"); } using var limitedFullyStringRa = CaptureToRectArea(); var limitedFullyStringRaocrList = limitedFullyStringRa.FindMulti(RecognitionObject.Ocr(0, 0, limitedFullyStringRa.Width * 0.5, limitedFullyStringRa.Height)); var limitedFullyStringRaocrListdone = limitedFullyStringRaocrList.LastOrDefault(t => Regex.IsMatch(t.Text, this.limitedFullyString) || Regex.IsMatch(t.Text, this.limitedFullyAllString)); // 检测是否为限时全开秘境 if (limitedFullyStringRaocrListdone != null) { Logger.LogInformation("自动秘境:{Text}", "检测到秘境限时全开"); } var serverTime = ServerTimeHelper.GetServerTimeNow(); if (serverTime is { DayOfWeek: DayOfWeek.Sunday, Hour: >= 4 } || serverTime is { DayOfWeek: DayOfWeek.Monday, Hour: < 4 } || limitedFullyStringRaocrListdone != null) { using var artifactArea = CaptureToRectArea().Find(fightAssets.ArtifactAreaRa); //检测是否为圣遗物副本 if (artifactArea.IsEmpty()) { if (int.TryParse(_taskParam.SundaySelectedValue, out int sundaySelectedValue)) { if (sundaySelectedValue > 0) { Logger.LogInformation(limitedFullyStringRaocrListdone != null ? "自动秘境:限时全开秘境奖励序号 {sundaySelectedValue}" : "自动秘境:周日设置了秘境奖励序号 {sundaySelectedValue}", sundaySelectedValue); using var abnormalscreenRa = CaptureToRectArea(); GlobalMethod.MoveMouseTo(abnormalscreenRa.Width / 4, abnormalscreenRa.Height / 2); //移到左侧 for (var i = 0; i < 100; i++) { Simulation.SendInput.Mouse.VerticalScroll(-1); await Delay(10, _ct); } await Delay(400, _ct); using var abnormalRa = CaptureToRectArea(); var ocrList = abnormalRa.FindMulti(RecognitionObject.Ocr(0, 0, abnormalRa.Width * 0.5, abnormalRa.Height)); var done = ocrList.LastOrDefault(t => Regex.IsMatch(t.Text, this.leyLineDisorderLocalizedString)); if (done != null) { await Delay(300, _ct); switch (sundaySelectedValue) { case 1: GlobalMethod.Click(done.X, done.Y - abnormalRa.Height / 5); break; case 2: GlobalMethod.Click(done.X, done.Y - abnormalRa.Height / 10); break; case 3: GlobalMethod.Click(done.X, done.Y); break; default: Logger.LogWarning("无效的 sundaySelectedValue 值: {sundaySelectedValue}", sundaySelectedValue); break; } } } else { Logger.LogInformation(limitedFullyStringRaocrListdone != null ? "自动秘境:限时全开秘境未设置特定秘境奖励" : "自动秘境:周日秘境未设置特定秘境奖励"); } } else { Logger.LogWarning(_taskParam.SundaySelectedValue == "" ? "未设置秘境奖励序号" : "设置秘境奖励序号错误,请检查配置页面"); } } await Delay(300, _ct); } // 点击单人挑战确认并等待队伍界面--使用图像模版匹配的方法,也可以使用文字OCR的方法识别“单人挑战”直到消失 await NewRetry.WaitForElementAppear( ElementAssets.Instance.PartyBtnChooseView, async void () => { using var ra = CaptureToRectArea(); var ra2 = ra.Find(fightAssets.ConfirmRa); if (!ra2.IsEmpty()) { ra2.Click(); 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("是否仍要挑战该秘境")) { Logger.LogWarning("自动秘境:检测到树脂不足提示:{Text}", confirmRectArea2.Text); throw new Exception("当前树脂不足,自动秘境停止运行。"); } }, _ct, 10, 1000 ); // 等待队伍选择界面出现 var teamUiFound = await NewRetry.WaitForElementAppear( ElementAssets.Instance.PartyBtnChooseView, () => { Logger.LogInformation("自动秘境:进入 {Text}", "队伍选择界面"); }, _ct, 10, 1000 ); if (!teamUiFound) { Logger.LogWarning("队伍选择界面未出现,跳过切换队伍。"); } else { await SwitchParty(_taskParam.PartyName); } // 点击开始挑战确认并等待“开始挑战”文字消失 var startFightFound = await NewRetry.WaitForElementDisappear( GetConfirmRa("开始挑战"), screen => { screen.Find(fightAssets.ConfirmRa, ra => { ra.Click(); ra.Dispose(); Logger.LogInformation("自动秘境:点击 {Text}", "开始挑战"); }); }, _ct, 10, 1000 ); if (!startFightFound) { Logger.LogWarning("开始挑战按钮未出现或未能点击。"); } // 载入 await Delay(1000, _ct); } private async Task CloseDomainTip() { //先等待秘境提示出现,如果直接出现Enter也属于完成条件 var domainTipFound = await NewRetry.WaitForAction(() => { using var ra = CaptureToRectArea(); var ocrList = ra.FindMulti(RecognitionObject.Ocr(0, ra.Height * 0.2, ra.Width, ra.Height * 0.6)); var ocrListLeft = ra.Find(AutoFightAssets.Instance.AbnormalIconRa); return (ocrList.Any(t => t.Text.Contains(leyLineDisorderLocalizedString) || t.Text.Contains(clickanywheretocloseLocalizedString))) || ocrListLeft.IsExist(); }, _ct, 40, 500); if (!domainTipFound) { Logger.LogWarning("秘境提示未出现或未能点击。"); } //持续点击,直到左下角出现目标文字 var leftBottomFound = await NewRetry.WaitForAction(() => { using var ra = CaptureToRectArea(); var ocrList = ra.FindMulti(RecognitionObject.Ocr(0, ra.Height * 0.2, ra.Width, ra.Height * 0.6)); // 查找目标文字 var done = ocrList.FirstOrDefault(t => Regex.IsMatch(t.Text, this.leyLineDisorderLocalizedString) || Regex.IsMatch(t.Text, this.clickanywheretocloseLocalizedString)); if (done != null) { done.Click(); done.Dispose(); Logger.LogInformation("自动秘境:点击 {Text}", done.Text); } // 检查左下角区域是否还存在目标文字,消失则继续,存在则结束 using var leftBottom = CaptureToRectArea(); var leftBottomOcr = leftBottom.Find(AutoFightAssets.Instance.AbnormalIconRa); return leftBottomOcr.IsExist(); }, _ct, 20, 500); if (!leftBottomFound) { //尝试随意点击一下右下角 GameCaptureRegion.GameRegion1080PPosClick(1515, 892); Logger.LogWarning("秘境提示未出现或未能点击。"); } await Delay(500, _ct); } private List FindCombatScriptAndSwitchAvatar(CombatScenes combatScenes) { var combatCommands = _combatScriptBag.FindCombatScript(combatScenes.GetAvatars()); var avatar = combatScenes.SelectAvatar(combatCommands[0].Name); avatar?.SwitchWithoutCts(); Sleep(200); return combatCommands; } /// /// 走到钥匙处启动 /// private async Task WalkToPressF() { if (_ct.IsCancellationRequested) { return; } await Task.Run((Action)(() => { Simulation.SendInput.SimulateAction(GIActions.MoveForward, KeyType.KeyDown); Sleep(30, _ct); // 组合键好像不能直接用 postmessage if (!_config.WalkToF) { Simulation.SendInput.SimulateAction(GIActions.SprintKeyboard, KeyType.KeyDown); } try { var startTime = DateTime.Now; while (!_ct.IsCancellationRequested) { using var fRectArea = Common.TaskControl.CaptureToRectArea().Find(AutoPickAssets.Instance.PickRo); if (fRectArea.IsEmpty()) { Sleep(100, _ct); } else { Logger.LogInformation("检测到交互键"); Simulation.SendInput.Keyboard.KeyPress(AutoPickAssets.Instance.PickVk); break; } // 超时直接放弃整个秘境 if (DateTime.Now - startTime > TimeSpan.FromSeconds(60)) { Logger.LogWarning("自动秘境:{Text}", "前往目标位置处超时,如果选择了秘境名称,将在传送后重试秘境!"); Avatar.TpForRecover(_ct, new RetryException("前往目标位置处超时,先传送到七天神像,然后重试秘境")); } } } finally { Simulation.SendInput.SimulateAction(GIActions.MoveForward, KeyType.KeyUp); Sleep(50); if (!_config.WalkToF) { Simulation.SendInput.SimulateAction(GIActions.SprintKeyboard, KeyType.KeyUp); } } }), _ct); } private Task StartFight(CombatScenes combatScenes, List combatCommands) { CancellationTokenSource cts = new(); _ct.Register(cts.Cancel); combatScenes.BeforeTask(cts.Token); // 战斗操作 var combatTask = new Task(() => { try { AutoFightTask.FightStatusFlag = true; while (!cts.Token.IsCancellationRequested) { // 通用化战斗策略 foreach (var command in combatCommands) { command.Execute(combatScenes); } } } catch (NormalEndException e) { Logger.LogInformation("战斗操作中断:{Msg}", e.Message); } catch (Exception e) { Logger.LogWarning(e.Message); throw; } finally { Logger.LogInformation("自动战斗线程结束"); Simulation.ReleaseAllKey(); AutoFightTask.FightStatusFlag = false; } }, cts.Token); // 对局结束检测 var domainEndTask = DomainEndDetectionTask(cts); // 自动吃药 // var autoEatRecoveryHpTask = AutoEatRecoveryHpTask(cts.Token); combatTask.Start(); domainEndTask.Start(); // autoEatRecoveryHpTask.Start(); return Task.WhenAll(combatTask, domainEndTask); } private void EndFightWait() { if (_ct.IsCancellationRequested) { return; } var s = TaskContext.Instance().Config.AutoDomainConfig.FightEndDelay; if (s > 0) { Logger.LogInformation("战斗结束后等待 {Second} 秒", s); Sleep((int)(s * 1000), _ct); } } /// /// 对局结束检测 /// private Task DomainEndDetectionTask(CancellationTokenSource cts) { return new Task(async () => { try { while (!_ct.IsCancellationRequested) { if (IsDomainEnd()) { await cts.CancelAsync(); break; } await Delay(1000, cts.Token); } } catch { } }, cts.Token); } private bool IsDomainEnd() { using var ra = CaptureToRectArea(); var endTipsRect = ra.DeriveCrop(AutoFightAssets.Instance.EndTipsUpperRect); var text = OcrFactory.Paddle.Ocr(endTipsRect.SrcMat); if (Regex.IsMatch(text, this.challengeCompletedLocalizedString)) { Logger.LogInformation("检测到秘境结束提示(挑战达成),结束秘境"); return true; } endTipsRect = ra.DeriveCrop(AutoFightAssets.Instance.EndTipsRect); text = OcrFactory.Paddle.Ocr(endTipsRect.SrcMat); if (Regex.IsMatch(text, this.autoLeavingLocalizedString)) { Logger.LogInformation("检测到秘境结束提示(xxx秒后自动退出),结束秘境"); return true; } return false; } private Task AutoEatRecoveryHpTask(CancellationToken ct) { return new Task(async () => { if (!_config.AutoEat) { return; } if (!IsTakeFood()) { Logger.LogInformation("未装备 “{Tool}”,不启用红血自动吃药功能", "便携营养袋"); return; } try { while (!_ct.IsCancellationRequested) { if (Bv.CurrentAvatarIsLowHp(CaptureToRectArea())) { // 模拟按键 "Z" Simulation.SendInput.SimulateAction(GIActions.QuickUseGadget); Logger.LogInformation("检测到红血,按Z吃药"); // TODO 吃饱了会一直吃 } await Delay(500, ct); } } catch (Exception e) { Logger.LogDebug(e, "红血自动吃药检测时发生异常"); } }, ct); } private bool IsTakeFood() { // 获取图像 using var ra = CaptureToRectArea(); // 识别道具图标下是否是数字 var s = TaskContext.Instance().SystemInfo.AssetScale; var countArea = ra.DeriveCrop(1800 * s, 845 * s, 40 * s, 20 * s); var count = OcrFactory.Paddle.OcrWithoutDetector(countArea.CacheGreyMat); return int.TryParse(count, out _); } /// /// 旋转视角后寻找石化古树 /// private Task FindPetrifiedTree() { CancellationTokenSource treeCts = new(); _ct.Register(treeCts.Cancel); // 中键回正视角 Simulation.SendInput.Mouse.MiddleButtonClick(); Sleep(900, _ct); // 左右移动直到石化古树位于屏幕中心任务 var moveAvatarTask = MoveAvatarHorizontallyTask(treeCts); // 锁定东方向视角线程 var lockCameraToEastTask = LockCameraToEastTask(treeCts, moveAvatarTask); lockCameraToEastTask.Start(); return Task.WhenAll(moveAvatarTask, lockCameraToEastTask); } private Task MoveAvatarHorizontallyTask(CancellationTokenSource treeCts) { return new Task(() => { var keyConfig = TaskContext.Instance().Config.KeyBindingsConfig; var moveLeftKey = keyConfig.MoveLeft.ToVK(); var moveRightKey = keyConfig.MoveRight.ToVK(); var moveForwardKey = keyConfig.MoveForward.ToVK(); var captureArea = TaskContext.Instance().SystemInfo.ScaleMax1080PCaptureRect; var middleX = captureArea.Width / 2; var leftKeyDown = false; var rightKeyDown = false; var noDetectCount = 0; var prevKey = moveLeftKey; var backwardsAndForwardsCount = 0; while (!_ct.IsCancellationRequested) { var treeRect = DetectTree(CaptureToRectArea()); if (treeRect != default) { var treeMiddleX = treeRect.X + treeRect.Width / 2; if (treeRect.X + treeRect.Width < middleX && !_config.ShortMovement) { backwardsAndForwardsCount = 0; // 树在左边 往左走 Debug.WriteLine($"树在左边 往左走 {treeMiddleX} {middleX}"); if (rightKeyDown) { // 先松开D键 Simulation.SendInput.Keyboard.KeyUp(moveRightKey); rightKeyDown = false; } if (!leftKeyDown) { Simulation.SendInput.Keyboard.KeyDown(moveLeftKey); leftKeyDown = true; } } else if (treeRect.X > middleX && !_config.ShortMovement) { backwardsAndForwardsCount = 0; // 树在右边 往右走 Debug.WriteLine($"树在右边 往右走 {treeMiddleX} {middleX}"); if (leftKeyDown) { // 先松开A键 Simulation.SendInput.Keyboard.KeyUp(moveLeftKey); leftKeyDown = false; } if (!rightKeyDown) { Simulation.SendInput.Keyboard.KeyDown(moveRightKey); rightKeyDown = true; } } else { // 树在中间 松开所有键 if (rightKeyDown) { Simulation.SendInput.Keyboard.KeyUp(moveRightKey); prevKey = moveRightKey; rightKeyDown = false; } if (leftKeyDown) { Simulation.SendInput.Keyboard.KeyUp(moveLeftKey); prevKey = moveLeftKey; leftKeyDown = false; } // 松开按键后使用小碎步移动 if (treeMiddleX < middleX) { if (prevKey == moveRightKey) { backwardsAndForwardsCount++; } Simulation.SendInput.Keyboard.KeyDown(moveLeftKey); Sleep(60); Simulation.SendInput.Keyboard.KeyUp(moveLeftKey); prevKey = moveLeftKey; } else if (treeMiddleX > middleX) { if (prevKey == moveLeftKey) { backwardsAndForwardsCount++; } Simulation.SendInput.Keyboard.KeyDown(moveRightKey); Sleep(60); Simulation.SendInput.Keyboard.KeyUp(moveRightKey); prevKey = moveRightKey; } else { Simulation.SendInput.Keyboard.KeyDown(moveForwardKey); Sleep(60); Simulation.SendInput.Keyboard.KeyUp(moveForwardKey); Sleep(500, _ct); treeCts.Cancel(); break; } } } else { backwardsAndForwardsCount = 0; // 左右巡逻 noDetectCount++; if (noDetectCount > 40) { if (leftKeyDown) { Simulation.SendInput.Keyboard.KeyUp(moveLeftKey); leftKeyDown = false; } if (!rightKeyDown) { Simulation.SendInput.Keyboard.KeyDown(moveRightKey); rightKeyDown = true; } } else { if (rightKeyDown) { Simulation.SendInput.Keyboard.KeyUp(moveRightKey); rightKeyDown = false; } if (!leftKeyDown) { Simulation.SendInput.Keyboard.KeyDown(moveLeftKey); leftKeyDown = true; } } } if (backwardsAndForwardsCount >= _config.LeftRightMoveTimes) { // 左右移动5次说明已经在树中心了 Simulation.SendInput.Keyboard.KeyDown(moveForwardKey); Sleep(60); Simulation.SendInput.Keyboard.KeyUp(moveForwardKey); Sleep(500, _ct); treeCts.Cancel(); break; } Sleep(60, _ct); } VisionContext.Instance().DrawContent.ClearAll(); }); } private Rect DetectTree(ImageRegion region) { var result = _predictor.Predictor.Detect(region.CacheImage); var list = new List(); foreach (var box in result) { var rect = new Rect(box.Bounds.X, box.Bounds.Y, box.Bounds.Width, box.Bounds.Height); list.Add(region.ToRectDrawable(rect, "tree")); } VisionContext.Instance().DrawContent.PutOrRemoveRectList("TreeBox", list); if (list.Count > 0) { var box = result[0]; return new Rect(box.Bounds.X, box.Bounds.Y, box.Bounds.Width, box.Bounds.Height); } return default; } private Task LockCameraToEastTask(CancellationTokenSource cts, Task moveAvatarTask) { return new Task(() => { var continuousCount = 0; // 连续东方向次数 var started = false; while (!cts.Token.IsCancellationRequested) { using var captureRegion = CaptureToRectArea(); var angle = CameraOrientation.Compute(captureRegion.SrcMat); CameraOrientation.DrawDirection(captureRegion, angle); if (angle is >= 356 or <= 4) { // 算作对准了 continuousCount++; // 360 度 东方向视角 if (continuousCount > 5) { if (!started && moveAvatarTask.Status != TaskStatus.Running) { started = true; moveAvatarTask.Start(); } } } else { continuousCount = 0; } if (angle <= 180) { // 左移视角 var moveAngle = (int)Math.Round(angle); if (moveAngle > 2) { moveAngle *= 2; } Simulation.SendInput.Mouse.MoveMouseBy(-moveAngle, 0); } else if (angle is > 180 and < 360) { // 右移视角 var moveAngle = 360 - (int)Math.Round(angle); if (moveAngle > 2) { moveAngle *= 2; } Simulation.SendInput.Mouse.MoveMouseBy(moveAngle, 0); } Sleep(100, _ct); } Logger.LogInformation("锁定东方向视角线程结束"); VisionContext.Instance().DrawContent.ClearAll(); }); } /// /// 领取奖励 /// private async Task GettingTreasure() { bool isLastTurn = false; // 等待窗口弹出 await Delay(300, _ct); // 1. OCR 直到确认弹出框弹出 bool chooseResinPrompt = await NewRetry.WaitForAction(() => { using var ra = CaptureToRectArea(); var regionList = ra.FindMulti(RecognitionObject.Ocr(ra.Width * 0.25, ra.Height * 0.2, ra.Width * 0.5, ra.Height * 0.6)); var res = regionList.FirstOrDefault(t => t.Text.Contains("石化古树")); if (res != null) { // 解决水龙王按下左键后没松开,然后后续点击按下就没反应了,界面上点一下 res.Click(); return true; } return false; }, _ct, 10, 500); Debug.WriteLine("识别到选择树脂页"); await Delay(800, _ct); // 再 OCR 一次,弹出框,确认当前是否有原粹树脂 using var ra2 = CaptureToRectArea(); var textListInPrompt = ra2.FindMulti(RecognitionObject.Ocr(ra2.Width * 0.25, ra2.Height * 0.2, ra2.Width * 0.5, ra2.Height * 0.6)); if (textListInPrompt.Any(t => t.Text.Contains("数量不足") || t.Text.Contains("补充原粹树脂"))) { // 没有原粹树脂,直接退出秘境 Logger.LogInformation("自动秘境:原粹树脂已用尽,退出秘境"); await ExitDomain(); return false; } if (chooseResinPrompt) { using var ra3 = CaptureToRectArea(); if (!_taskParam.SpecifyResinUse) { // 自动刷干树脂 // 识别树脂状况 var resinStatus = ResinStatus.RecogniseFromRegion(ra3, TaskContext.Instance().SystemInfo, OcrFactory.Paddle); resinStatus.Print(Logger); if (resinStatus is { CondensedResinCount: <= 0, OriginalResinCount: < 20 }) { Logger.LogWarning("树脂不足"); await ExitDomain(); return false; } bool resinUsed = false; if (resinStatus.CondensedResinCount > 0) { (resinUsed, _) = PressUseResin(ra3, "浓缩树脂"); resinStatus.CondensedResinCount -= 1; } else if (resinStatus.OriginalResinCount >= 20) { (resinUsed, var num) = PressUseResin(ra3, "原粹树脂"); resinStatus.OriginalResinCount -= num; } if (!resinUsed) { Logger.LogWarning("自动秘境:未找到可用的树脂,可能是{Msg1} 或者 {Msg2}。", "树脂不足", "OCR 识别失败"); await ExitDomain(); return false; } if (resinStatus is { CondensedResinCount: <= 0, OriginalResinCount: < 20 }) { // 没树脂了就是最后一回合了 isLastTurn = true; } } else { // 指定使用树脂 var textListInPrompt2 = ra3.FindMulti(RecognitionObject.Ocr(ra3.Width * 0.25, ra3.Height * 0.2, ra3.Width * 0.5, ra3.Height * 0.6)); // 按优先级使用 int successCount = 0; foreach (var record in _resinPriorityListWhenSpecifyUse) { if (record.RemainCount > 0) { 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; } } } if (_resinPriorityListWhenSpecifyUse.Sum(o => o.RemainCount) <= 0) { // 全部刷完 isLastTurn = true; } 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; } } } else { // 如果没有选择树脂的提示,说明只有原粹树脂 // 继续向下执行 } Sleep(1000, _ct); for (var i = 0; i < 30; i++) { using var ra = CaptureToRectArea(); // 优先点击继续 using var confirmRectArea = ra.Find(AutoFightAssets.Instance.ConfirmRa); if (!confirmRectArea.IsEmpty()) { if (isLastTurn) { // 最后一回合 退出 var exitRectArea = ra.Find(AutoFightAssets.Instance.ExitRa); if (!exitRectArea.IsEmpty()) { exitRectArea.Click(); return false; } } else { if (!chooseResinPrompt) { // TODO 前面没有弹框的情况下,意味着只有原粹树脂,要再识别一次右上角确认树脂余量,没有余量直接退出 } // 有体力继续 confirmRectArea.Click(); await Delay(60, _ct); // 双击 confirmRectArea.Click(); if (!chooseResinPrompt) { // 真没树脂了还有提示兜底 await Delay(900, _ct); var textListInNoResinPrompt = CaptureToRectArea().FindMulti(RecognitionObject.Ocr(ra2.Width * 0.25, ra2.Height * 0.2, ra2.Width * 0.5, ra2.Height * 0.6)); if (textListInNoResinPrompt.Any(t => t.Text.Contains("是否仍要") && t.Text.Contains("挑战") && t.Text.Contains("秘境"))) { var cancelBtn = textListInNoResinPrompt.FirstOrDefault(t => t.Text.Contains("取消")); if (cancelBtn != null) { cancelBtn.Click(); return false; } } } return true; } } Sleep(300, _ct); } throw new NormalEndException("未检测到秘境结束,可能是背包物品已满。"); } private async Task ExitDomain() { Simulation.SendInput.Keyboard.KeyPress(VK.VK_ESCAPE); await Delay(500, _ct); Simulation.SendInput.Keyboard.KeyPress(VK.VK_ESCAPE); await Delay(800, _ct); Bv.ClickBlackConfirmButton(CaptureToRectArea()); } 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); } public static (bool, int) 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); useKey.Click(); var num = GetResinNum(resinKey, resinName); Logger.LogInformation("自动秘境:使用 {ResinName}, 数量:{Num}", resinName, num); return (true, num); } else { Logger.LogWarning("自动秘境:未找到 {ResinName} 的使用按键", resinName); } } else { Logger.LogWarning("自动秘境:未找到 {ResinName} 的使用按键", resinName); } } 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 static 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) { return; } if (!int.TryParse(_taskParam.MaxArtifactStar, out var star)) { star = 4; } await new AutoArtifactSalvageTask(new AutoArtifactSalvageTaskParam(star, javaScript: null, artifactSetFilter: null, maxNumToCheck: null, recognitionFailurePolicy: null)).Start(_ct); } }