using BetterGenshinImpact.Core.Recognition; using BetterGenshinImpact.Core.Recognition.OpenCv; using BetterGenshinImpact.Core.Script.Dependence; using BetterGenshinImpact.Core.Simulator; using BetterGenshinImpact.Core.Simulator.Extensions; using BetterGenshinImpact.GameTask.AutoGeniusInvokation.Exception; using BetterGenshinImpact.GameTask.AutoPathing; using BetterGenshinImpact.GameTask.AutoPathing.Model; using BetterGenshinImpact.GameTask.AutoPathing.Model.Enum; using BetterGenshinImpact.GameTask.AutoTrackPath.Model; using BetterGenshinImpact.GameTask.Common; using BetterGenshinImpact.GameTask.Common.BgiVision; using BetterGenshinImpact.GameTask.Common.Element.Assets; using BetterGenshinImpact.GameTask.Common.Exceptions; using BetterGenshinImpact.GameTask.Common.Job; using BetterGenshinImpact.GameTask.Common.Map.Maps; using BetterGenshinImpact.GameTask.Common.Map.Maps.Base; using BetterGenshinImpact.GameTask.Model.Area; using BetterGenshinImpact.GameTask.QuickTeleport.Assets; using BetterGenshinImpact.Helpers; using BetterGenshinImpact.Helpers.Extensions; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; using OpenCvSharp; using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; using Vanara.PInvoke; using static BetterGenshinImpact.GameTask.Common.TaskControl; namespace BetterGenshinImpact.GameTask.AutoTrackPath; /// /// 传送任务 /// public class TpTask { private readonly QuickTeleportAssets _assets = QuickTeleportAssets.Instance; 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; /// /// 直接通过缩放比例按钮计算放大按钮的Y坐标 /// private readonly int _zoomInButtonY = TaskContext.Instance().Config.TpConfig.ZoomStartY - 24; // y-coordinate for zoom-in button = _zoomStartY - 24 /// /// 直接通过缩放比例按钮计算缩小按钮的Y坐标 /// private readonly int _zoomOutButtonY = TaskContext.Instance().Config.TpConfig.ZoomEndY + 24; // y-coordinate for zoom-out button = _zoomEndY + 24 private const double DisplayTpPointZoomLevel = 4.4; // 传送点显示的时候的地图比例 public TpTask(CancellationToken ct) { this.ct = ct; TpTaskParam param = new TpTaskParam(); this.cultureInfo = param.GameCultureInfo; this.stringLocalizer = param.StringLocalizer; } /// /// 传送到七天神像 /// public async Task TpToStatueOfTheSeven() { await CheckInBigMapUi(); // 提前调整至恰当的缩放以更快的传送 if (_tpConfig.MapZoomEnabled) { double currentZoomLevel = GetBigMapZoomLevel(CaptureToRectArea()); if (currentZoomLevel > DisplayTpPointZoomLevel) { await AdjustMapZoomLevel(currentZoomLevel, DisplayTpPointZoomLevel); } else if (currentZoomLevel < 3) { await AdjustMapZoomLevel(currentZoomLevel, 3); } } string? country = _tpConfig.ReviveStatueOfTheSevenCountry; string? area = _tpConfig.ReviveStatueOfTheSevenArea; double x = _tpConfig.ReviveStatueOfTheSevenPointX; double y = _tpConfig.ReviveStatueOfTheSevenPointY; GiTpPosition revivePoint = _tpConfig.ReviveStatueOfTheSeven ?? GetNearestGoddess(x, y); if (_tpConfig.IsReviveInNearestStatueOfTheSeven) { var center = GetBigMapCenterPoint(MapTypes.Teyvat.ToString()); var giTpPoint = GetNearestGoddess(center.X, center.Y); country = giTpPoint.Country; area = giTpPoint.Level1Area; x = giTpPoint.X; y = giTpPoint.Y; revivePoint = giTpPoint; } Logger.LogInformation("将传送至 {country} {area} 七天神像", country, area); await Tp(x, y, MapTypes.Teyvat.ToString(), false); if (_tpConfig.ShouldMove || _tpConfig.IsReviveInNearestStatueOfTheSeven) { (x, y) = GetClosestPoint(revivePoint.TranX, revivePoint.TranY, x, y, 5); var waypoint = new Waypoint { X = x, Y = y, Type = WaypointType.Path.Code, MoveMode = MoveModeEnum.Walk.Code }; var waypointForTrack = new WaypointForTrack(waypoint, nameof(MapTypes.Teyvat), _mapMatchingMethod); await new PathExecutor(ct).MoveTo(waypointForTrack); Simulation.SendInput.SimulateAction(GIActions.Drop); } await Delay((int)(_tpConfig.HpRestoreDuration * 1000), ct); } /// /// /// /// 传送后实际到达的点X坐标 /// 传送后实际到达的点Y坐标 /// 传送点 X 坐标 /// 传送点 Y 坐标 /// 期望最终离传送点的距离 /// private static (double X, double Y) GetClosestPoint(double tranX, double tranY, double x, double y, double d) { double dx = x - tranX; double dy = y - tranY; double distanceSquared = dx * dx + dy * dy; double distance = Math.Sqrt(distanceSquared); d = d > 0 ? d : 0; if (distance < d) { return (tranX, tranY); } double ratio = d / distance; double px = (x - dx * ratio); double py = (y - dy * ratio); return (px, py); } /// /// 获取离 x,y 最近的七天神像 /// /// /// /// private GiTpPosition GetNearestGoddess(double x, double y) { GiTpPosition? nearestGiTpPosition = null; double minDistance = double.MaxValue; foreach (var (_, goddessPosition) in MapLazyAssets.Instance.GoddessPositions) { var distance = Math.Sqrt(Math.Pow(goddessPosition.X - x, 2) + Math.Pow(goddessPosition.Y - y, 2)); if (distance < minDistance) { minDistance = distance; nearestGiTpPosition = goddessPosition; } } // 获取最近的神像位置 return nearestGiTpPosition ?? throw new InvalidOperationException("没找到最近的七天神像"); } /// ///释放所有按键,并打开大地图界面 /// /// 重试次数 public async Task OpenBigMapUi(int retryCount = 3) { for (var i = 0; i < retryCount; i++) { try { // 打开地图前释放所有按键 Simulation.ReleaseAllKey(); await Delay(20, ct); await CheckInBigMapUi(); return; } catch (Exception e) when (e is NormalEndException || e is TaskCanceledException) { throw; } catch (Exception e) { if (retryCount > 1) { Logger.LogError("打开大地图失败,重试 {I} 次", i + 1); Logger.LogDebug(e, "打开大地图失败,重试 {I} 次", i + 1); } if (i + 1 >= retryCount) { throw; } } } } /// /// 通过大地图传送到指定坐标最近的传送点,然后移动到指定坐标 /// /// /// /// 独立地图名称 /// 强制以当前的tpX,tpY坐标进行自动传送 private async Task<(double, double)> TpOnce(double tpX, double tpY, string mapName = "Teyvat", bool force = false) { // 1. 确认在地图界面 await OpenBigMapUi(1); // 2. 传送前的计算准备 // 获取离目标传送点最近的两个传送点,按距离排序 var nTpPoints = GetNearestNTpPoints(tpX, tpY, mapName, 2); // 获取最近的传送点与区域 var (x, y, country) = force ? (tpX, tpY, null) : (nTpPoints[0].X, nTpPoints[0].Y, nTpPoints[0].Country); var disBetweenTpPoints = Math.Sqrt(Math.Pow(nTpPoints[0].X - nTpPoints[1].X, 2) + Math.Pow(nTpPoints[0].Y - nTpPoints[1].Y, 2)); // 确保不会点错传送点的最小缩放,保证至少为 1.0 var minZoomLevel = Math.Max(disBetweenTpPoints / 20, 1.0); // 切换地区 if (mapName == MapTypes.Teyvat.ToString()) { // 计算传送点位置离哪张地图切换后的中心点最近,切换到该地图 await SwitchRecentlyCountryMap(x, y, country); } else { // 直接切换地区 await SwitchArea(MapTypesExtensions.ParseFromName(mapName).GetDescription()); } // 3. 调整初始缩放等级,避免识别中心点失败 var zoomLevel = GetBigMapZoomLevel(CaptureToRectArea()); if (_tpConfig.MapZoomEnabled) { /* 动态调整缩放逻辑: 1. 如果当前缩放大于显示传送点级别 -> 缩小 2. 如果小于配置的最小级别 -> 放大 */ if (zoomLevel > DisplayTpPointZoomLevel + _tpConfig.PrecisionThreshold) { await AdjustMapZoomLevel(zoomLevel, DisplayTpPointZoomLevel); zoomLevel = DisplayTpPointZoomLevel; Logger.LogInformation("当前缩放等级过大,调整为 {zoomLevel:0.00}", DisplayTpPointZoomLevel); } else if (zoomLevel < _tpConfig.MinZoomLevel - _tpConfig.PrecisionThreshold) { await AdjustMapZoomLevel(zoomLevel, _tpConfig.MinZoomLevel); zoomLevel = _tpConfig.MinZoomLevel; Logger.LogInformation("当前缩放等级过小,调整为 {zoomLevel:0.00}", _tpConfig.MinZoomLevel); } } // 4. zoomLevel不满足条件,强制进行一次 MoveMapTo,避免传送点相近导致误点 if (zoomLevel > minZoomLevel) { if (_tpConfig.MapZoomEnabled) { Logger.LogInformation("目标传送点有相近传送点,到目标传送点附近将缩放到{zoomLevel:0.00}", minZoomLevel); await MoveMapTo(x, y, mapName, minZoomLevel); await Delay(300, ct); // 等待地图移动完成 } else { Logger.LogInformation("目标传送点有相近传送点,可能传送失败。如果失败请到设置-大地图地图传送设置开启地图缩放"); // TODO 部分无法区分点位强制缩放,即使没有zoomEnabled。 } } // 5. 判断传送点是否在当前界面,若否则移动地图 var bigMapInAllMapRect = GetBigMapRect(mapName); var retryCount = 0; do { if (IsPointInBigMapWindow(mapName, bigMapInAllMapRect, x, y)) break; if (retryCount++ >= 5) // 防止死循环 { Logger.LogWarning("多次尝试未移动到目标传送点,传送失败"); throw new Exception("多次尝试未移动到目标传送点,传送失败"); } Logger.LogInformation("传送点不在当前大地图范围内,重新调整地图位置"); await MoveMapTo(x, y, mapName); await Delay(300, ct); bigMapInAllMapRect = GetBigMapRect(mapName); } while (true); // 6. 计算传送点位置并点击 // Debug.WriteLine($"({x},{y}) 在 {bigMapInAllMapRect} 内,计算它在窗体内的位置"); // 注意这个坐标的原点是中心区域某个点,所以要转换一下点击坐标(点击坐标是左上角为原点的坐标系),不能只是缩放 var (clickX, clickY) = ConvertToGameRegionPosition(mapName, bigMapInAllMapRect, x, y); Logger.LogInformation("点击传送点"); CaptureToRectArea().ClickTo((int)clickX, (int)clickY); // 7. 触发一次快速传送功能 await Delay(500, ct); await ClickTpPoint(CaptureToRectArea()); // 8. 等待传送完成 await WaitForTeleportCompletion(50, 1200); return (x, y); } /// /// 检查传送是否完成,未完成则等待 /// /// 最大检查延时的次数 /// 如果未完成加载,检查加载页面的延时。 private async Task WaitForTeleportCompletion(int maxAttempts, int delayMs) { await Delay(delayMs, ct); for (var i = 0; i < maxAttempts; i++) { using var capture = CaptureToRectArea(); if (Bv.IsInMainUi(capture)) { Logger.LogInformation("传送完成,返回主界面"); return; } //增加容错,小概率情况下碰到,前面点击传送失败 capture.Find(_assets.TeleportButtonRo, rg => rg.Click()); await Delay(delayMs, ct); // 打开大地图期间推送的月卡会在传送之后直接显示,导致检测不到传送完成。 await _blessingOfTheWelkinMoonTask.Start(ct); } Logger.LogWarning("传送等待超时,换台电脑吧"); } /// /// 传送点是否在大地图窗口内 /// /// /// 大地图在整个游戏地图中的矩形位置(原神坐标系) /// 传送点x坐标(原神坐标系) /// 传送点y坐标(原神坐标系) /// private bool IsPointInBigMapWindow(string mapName, Rect bigMapInAllMapRect, double x, double y) { // 坐标不包含直接返回 if (!bigMapInAllMapRect.Contains(x, y)) { return false; } var (clickX, clickY) = ConvertToGameRegionPosition(mapName, bigMapInAllMapRect, x, y); // 屏蔽左上角360x400区域 if (clickX < 360 * _zoomOutMax1080PRatio && clickY < 400 * _zoomOutMax1080PRatio) { return false; } // 屏蔽周围 115 一圈的区域 if (clickX < 115 * _zoomOutMax1080PRatio || clickY < 115 * _zoomOutMax1080PRatio || clickX > _captureRect.Width - 115 * _zoomOutMax1080PRatio || clickY > _captureRect.Height - 115 * _zoomOutMax1080PRatio) { return false; } return true; } /// /// 转换传送点坐标到窗体内需要点击的坐标 /// /// /// 大地图在整个游戏地图中的矩形位置(原神坐标系) /// 传送点x坐标(原神坐标系) /// 传送点y坐标(原神坐标系) /// private (double clickX, double clickY) ConvertToGameRegionPosition(string mapName, Rect bigMapInAllMapRect, double x, double y) { var (picX, picY) = MapManager.GetMap(mapName, _mapMatchingMethod).ConvertGenshinMapCoordinatesToImageCoordinates(new Point2f((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; return (clickX, clickY); } public async Task CheckInBigMapUi() { // 尝试打开地图失败后,先回到主界面后再次尝试打开地图 if (!await TryToOpenBigMapUi()) { await new ReturnMainUiTask().Start(ct); await Delay(500, ct); if (!await TryToOpenBigMapUi()) { throw new RetryException("打开大地图失败,请检查按键绑定中「打开地图」按键设置是否和原神游戏中一致!"); } } } /// /// 尝试打开地图界面 /// private async Task TryToOpenBigMapUi() { // M 打开地图识别当前位置,中心点为当前位置 var ra1 = CaptureToRectArea(); if (!Bv.IsInBigMapUi(ra1)) { Simulation.SendInput.SimulateAction(GIActions.OpenMap); await Delay(1000, ct); for (int i = 0; i < 3; i++) { ra1 = CaptureToRectArea(); if (!Bv.IsInBigMapUi(ra1)) { await Delay(500, ct); } else { return true; } } return false; } else { return true; } } public async Task<(double, double)> Tp(double tpX, double tpY, string mapName = "Teyvat", bool force = false) { for (var i = 0; i < 3; i++) { try { return await TpOnce(tpX, tpY, mapName, force); } catch (TpPointNotActivate e) { // 传送点未激活或不存在 按ESC回到大地图界面 Simulation.SendInput.Keyboard.KeyPress(User32.VK.VK_ESCAPE); await Delay(300, ct); // throw; // 不抛出异常,继续重试 Logger.LogWarning(e.Message + " 重试"); } catch (Exception e) when (e is NormalEndException || e is TaskCanceledException) { throw; } catch (Exception e) { Logger.LogError("传送失败,重试 {I} 次", i + 1); Logger.LogDebug(e, "传送失败,重试 {I} 次", i + 1); } } throw new InvalidOperationException("传送失败"); } /// /// 移动地图到指定传送点位置 /// 可能会移动不对,所以可以重试此方法 /// /// 目标x坐标 /// 目标y坐标 /// 地图名称 /// 到达目标点的最小缩放等级,只在 MapZoomEnabled 为 True 生效 public async Task MoveMapTo(double x, double y, string mapName, double finalZoomLevel = 2) { // 参数初始化 double minZoomLevel = Math.Min(finalZoomLevel, _tpConfig.MinZoomLevel); double maxZoomLevel = _tpConfig.MaxZoomLevel; double currentZoomLevel = GetBigMapZoomLevel(CaptureToRectArea()); int exceptionTimes = 0; Point2f mapCenterPoint; try { mapCenterPoint = GetPositionFromBigMap(mapName); // 初始中心 } catch (Exception e) { ++exceptionTimes; mapCenterPoint = new Point2f(0f, 0f); // 其他恰当的初始值? } var (xOffset, yOffset) = (x - mapCenterPoint.X, y - mapCenterPoint.Y); double totalMoveMouseX = _tpConfig.MapScaleFactor * Math.Abs(xOffset) / currentZoomLevel; double totalMoveMouseY = _tpConfig.MapScaleFactor * Math.Abs(yOffset) / currentZoomLevel; double mouseDistance = Math.Sqrt(totalMoveMouseX * totalMoveMouseX + totalMoveMouseY * totalMoveMouseY); // 缩小地图到恰当的缩放 if (_tpConfig.MapZoomEnabled) { if (mouseDistance > _tpConfig.MapZoomOutDistance) { double targetZoomLevel = currentZoomLevel * mouseDistance / _tpConfig.MapZoomOutDistance; targetZoomLevel = Math.Min(targetZoomLevel, maxZoomLevel); await AdjustMapZoomLevel(currentZoomLevel, targetZoomLevel); double nextZoomLevel = GetBigMapZoomLevel(CaptureToRectArea()); totalMoveMouseX *= currentZoomLevel / nextZoomLevel; totalMoveMouseY *= currentZoomLevel / nextZoomLevel; mouseDistance *= currentZoomLevel / nextZoomLevel; currentZoomLevel = nextZoomLevel; } } // 开始移动并放大地图 for (var iteration = 0; iteration < _tpConfig.MaxIterations; iteration++) { if (_tpConfig.MapZoomEnabled) { if (mouseDistance < _tpConfig.MapZoomInDistance) { double targetZoomLevel = currentZoomLevel * mouseDistance / _tpConfig.MapZoomInDistance; targetZoomLevel = Math.Max(targetZoomLevel, minZoomLevel); if (currentZoomLevel > minZoomLevel + _tpConfig.PrecisionThreshold) { await AdjustMapZoomLevel(currentZoomLevel, targetZoomLevel); double nextZoomLevel = GetBigMapZoomLevel(CaptureToRectArea()); totalMoveMouseX *= currentZoomLevel / nextZoomLevel; totalMoveMouseY *= currentZoomLevel / nextZoomLevel; mouseDistance *= currentZoomLevel / nextZoomLevel; currentZoomLevel = nextZoomLevel; } } } // 非常接近目标点,不再进一步调整 if (mouseDistance < _tpConfig.Tolerance) { Logger.LogDebug("移动 {I} 次鼠标后,已经接近目标点,不再移动地图。", iteration + 1); break; } int moveMouseX = (int)Math.Min(totalMoveMouseX, _tpConfig.MaxMouseMove * totalMoveMouseX / mouseDistance) * Math.Sign(xOffset); int moveMouseY = (int)Math.Min(totalMoveMouseY, _tpConfig.MaxMouseMove * totalMoveMouseY / mouseDistance) * Math.Sign(yOffset); double moveMouseLength = Math.Sqrt(moveMouseX * moveMouseX + moveMouseY * moveMouseY); int moveSteps = Math.Max((int)moveMouseLength / 10, 3); // 每次移动的步数最小为 3,避免除 0 错误 await MouseMoveMap(moveMouseX, moveMouseY, moveSteps); try { exceptionTimes = 0; mapCenterPoint = GetPositionFromBigMap(mapName); // 随循环更新的地图中心 } catch (Exception) { if (++exceptionTimes > 2) { throw new Exception("多次中心点识别失败,重新传送"); } Logger.LogWarning("中心点识别失败,预测移动的距离"); mapCenterPoint += new Point2f((float)(moveMouseX * currentZoomLevel / _tpConfig.MapScaleFactor), (float)(moveMouseY * currentZoomLevel / _tpConfig.MapScaleFactor)); } (xOffset, yOffset) = (x - mapCenterPoint.X, y - mapCenterPoint.Y); totalMoveMouseX = _tpConfig.MapScaleFactor * Math.Abs(xOffset) / currentZoomLevel; totalMoveMouseY = _tpConfig.MapScaleFactor * Math.Abs(yOffset) / currentZoomLevel; mouseDistance = Math.Sqrt(totalMoveMouseX * totalMoveMouseX + totalMoveMouseY * totalMoveMouseY); } } /// /// 点击并移动鼠标 /// /// 鼠标初始位置x /// 鼠标初始位置y /// 鼠标移动后位置x /// 鼠标移动后位置y public async Task MouseClickAndMove(int x1, int y1, int x2, int y2) { // GlobalMethod.MoveMouseTo(x1, y1); GameCaptureRegion.GameRegionMove((rect, scale) => (x1 * scale, y1 * scale)); await Delay(50, ct); GlobalMethod.LeftButtonDown(); await Delay(50, ct); // GlobalMethod.MoveMouseTo(x2, y2); GameCaptureRegion.GameRegionMove((rect, scale) => (x2 * scale, y2 * scale)); await Delay(50, ct); GlobalMethod.LeftButtonUp(); await Delay(50, ct); GameCaptureRegion.GameRegionMove((rect, scale) => (rect.Width / 2d, rect.Width / 2d)); } /// /// 调整地图缩放级别以加速移动 /// /// 是否放大地图 [Obsolete] private async Task AdjustMapZoomLevel(bool zoomIn) { if (zoomIn) { GameCaptureRegion.GameRegionClick((rect, scale) => (_tpConfig.ZoomButtonX * scale, _zoomInButtonY * scale)); } else { GameCaptureRegion.GameRegionClick((rect, scale) => (_tpConfig.ZoomButtonX * scale, _zoomOutButtonY * scale)); } await Delay(100, ct); } /// /// 调整地图的缩放等级(整数缩放级别)。 /// /// 目标等级:1-6。整数。随着数字变大地图越小,细节越少。 [Obsolete] public async Task AdjustMapZoomLevel(int zoomLevel) { for (int i = 0; i < 5; i++) { await AdjustMapZoomLevel(false); } await Delay(200, ct); for (int i = 0; i < 6 - zoomLevel; i++) { await AdjustMapZoomLevel(true); } } /// /// 将大地图缩放等级设置为指定值 /// /// /// 缩放等级说明: /// - 数值范围:1.0(最大地图) 到 6.0(最小地图) /// - 缩放效果:数值越大,地图显示范围越广,细节越少 /// - 缩放位置:1.0 对应缩放条最上方,6.0 对应缩放条最下方 /// - 推荐范围:建议在 2.0 到 5.0 之间调整,过大或过小可能影响操作 /// /// 当前缩放等级:1.0-6.0,浮点数。 /// 目标缩放等级:1.0-6.0,浮点数。 public async Task AdjustMapZoomLevel(double zoomLevel, double targetZoomLevel) { // Logger.LogInformation("调整地图缩放等级:{zoomLevel:0.000} -> {targetZoomLevel:0.000}", zoomLevel, targetZoomLevel); int initialY = (int)(_tpConfig.ZoomStartY + (_tpConfig.ZoomEndY - _tpConfig.ZoomStartY) * (zoomLevel - 1) / 5d); int targetY = (int)(_tpConfig.ZoomStartY + (_tpConfig.ZoomEndY - _tpConfig.ZoomStartY) * (targetZoomLevel - 1) / 5d); await MouseClickAndMove(_tpConfig.ZoomButtonX, initialY, _tpConfig.ZoomButtonX, targetY); await Delay(100, ct); } private async Task MouseMoveMap(int pixelDeltaX, int pixelDeltaY, int steps = 10) { double dpi = TaskContext.Instance().DpiScale; int[] stepX = GenerateSteps((int)(pixelDeltaX / dpi), steps); int[] stepY = GenerateSteps((int)(pixelDeltaY / dpi), steps); // 随机起点以避免地图移动无效 GameCaptureRegion.GameRegionMove((rect, _) => (rect.Width / 2d + Random.Shared.Next(-rect.Width / 6, rect.Width / 6), rect.Height / 2d + Random.Shared.Next(-rect.Height / 6, rect.Height / 6))); Simulation.SendInput.Mouse.LeftButtonDown(); for (var i = 0; i < steps; i++) { var i1 = i; await Delay(_tpConfig.StepIntervalMilliseconds, ct); // Simulation.SendInput.Mouse.MoveMouseBy(stepX[i], stepY[i]); GameCaptureRegion.GameRegionMoveBy((_, scale) => (stepX[i1] * scale, stepY[i1] * scale)); } Simulation.SendInput.Mouse.LeftButtonUp(); } private int[] GenerateSteps(int delta, int steps) { double[] factors = new double[steps]; double sum = 0; for (int i = 0; i < steps; i++) { factors[i] = Math.Cos(i * Math.PI / (2 * steps)); sum += factors[i]; } int[] stepsArr = new int[steps]; int remaining = delta; // 两阶段分配:基础值 + 余数补偿 for (int i = 0; i < steps; i++) { double ratio = factors[i] / sum; stepsArr[i] = (int)(delta * ratio); // 基础值 remaining -= stepsArr[i]; } int center = steps / 2; for (int r = 0; r < Math.Abs(remaining); r++) { int target = (center + r) % steps; // 从中点开始螺旋分配 stepsArr[target] += remaining > 0 ? 1 : -1; } return stepsArr; } public Point2f GetPositionFromBigMap(string mapName) { return GetBigMapCenterPoint(mapName); } public Point2f? GetPositionFromBigMapNullable(string mapName) { try { return GetBigMapCenterPoint(mapName); } catch { return null; } } public Rect GetBigMapRect(string mapName) { var rect = new Rect(); NewRetry.Do(() => { // 判断是否在地图界面 using var ra = CaptureToRectArea(); using var mapScaleButtonRa = ra.Find(QuickTeleportAssets.Instance.MapScaleButtonRo); if (mapScaleButtonRa.IsExist()) { rect = MapManager.GetMap(mapName, _mapMatchingMethod).GetBigMapRect(ra.CacheGreyMat); if (rect == default) { // 滚轮调整后再次识别 Simulation.SendInput.Mouse.VerticalScroll(2); Sleep(500); throw new RetryException("识别大地图位置失败"); } } else { throw new RetryException("当前不在地图界面"); } }, TimeSpan.FromMilliseconds(500), 5); if (rect == default) { throw new InvalidOperationException("多次重试后,识别大地图位置失败"); } Debug.WriteLine("识别大地图在全地图位置矩形:" + rect); // 提瓦特大陆由于用的256的图,需要做特殊逻辑 if (mapName == MapTypes.Teyvat.ToString()) { const int s = TeyvatMap.BigMap256ScaleTo2048; // 相对2048做8倍缩放 rect = new Rect(rect.X * s, rect.Y * s, rect.Width * s, rect.Height * s); } return MapManager.GetMap(mapName, _mapMatchingMethod).ConvertImageCoordinatesToGenshinMapCoordinates(rect)!.Value; } public Point2f GetBigMapCenterPoint(string mapName) { // 判断是否在地图界面 using var ra = CaptureToRectArea(); using var mapScaleButtonRa = ra.Find(QuickTeleportAssets.Instance.MapScaleButtonRo); if (mapScaleButtonRa.IsExist()) { var p = MapManager.GetMap(mapName, _mapMatchingMethod).GetBigMapPosition(ra.CacheGreyMat); if (p.IsEmpty()) { throw new InvalidOperationException("识别大地图位置失败"); } Debug.WriteLine("识别大地图在全地图位置:" + p); // 提瓦特大陆由于用的256的图,需要做特殊逻辑 var (x, y) = (p.X, p.Y); if (mapName == MapTypes.Teyvat.ToString()) { (x, y) = (p.X * TeyvatMap.BigMap256ScaleTo2048, p.Y * TeyvatMap.BigMap256ScaleTo2048); } return MapManager.GetMap(mapName, _mapMatchingMethod).ConvertImageCoordinatesToGenshinMapCoordinates(new Point2f(x, y))!.Value; } else { throw new InvalidOperationException("当前不在地图界面"); } } /// /// 获取最接近的N个传送点坐标和所处区域 /// /// /// /// 获取最近的 n 个传送点 /// public List GetNearestNTpPoints(double x, double y, string mapName, int n = 1) { // 检查 n 的合法性 if (n < 1) { throw new ArgumentException("The value of n must be greater than or equal to 1.", nameof(n)); } // 按距离排序并选择前 n 个点 return MapLazyAssets.Instance.ScenesDic[mapName].Points .OrderBy(tp => Math.Pow(tp.X - x, 2) + Math.Pow(tp.Y - y, 2)) .Take(n) .ToList(); } public async Task SwitchRecentlyCountryMap(double x, double y, string? forceCountry = null) { // 可能是地下地图,切换到地上地图 using var ra2 = CaptureToRectArea(); if (Bv.BigMapIsUnderground(ra2)) { ra2.Find(_assets.MapUndergroundToGroundButtonRo).Click(); await Delay(200, ct); } // 识别当前位置 var minDistance = double.MaxValue; var bigMapCenterPointNullable = GetPositionFromBigMapNullable(MapTypes.Teyvat.ToString()); if (bigMapCenterPointNullable != null) { var bigMapCenterPoint = bigMapCenterPointNullable.Value; Logger.LogDebug("识别当前大地图位置:{Pos}", bigMapCenterPoint); minDistance = Math.Sqrt(Math.Pow(bigMapCenterPoint.X - x, 2) + Math.Pow(bigMapCenterPoint.Y - y, 2)); if (minDistance < 50) { // 点位很近的情况下不切换 return false; } } string minCountry = "当前位置"; foreach (var (country, position) in MapLazyAssets.Instance.CountryPositions) { var distance = Math.Sqrt(Math.Pow(position[0] - x, 2) + Math.Pow(position[1] - y, 2)); if (distance < minDistance) { minDistance = distance; minCountry = country; } } Logger.LogDebug("离目标传送点最近的区域是:{Country}", minCountry); if (minCountry != "当前位置") { if (forceCountry != null) { minCountry = forceCountry; } await SwitchArea(minCountry); return true; } return false; } internal async Task SwitchArea(string areaName) { GameCaptureRegion.GameRegionClick((rect, scale) => (rect.Width - 160 * scale, rect.Height - 60 * scale)); await Delay(300, ct); using var ra = CaptureToRectArea(); var list = ra.FindMulti(new RecognitionObject { RecognitionType = RecognitionTypes.Ocr, RegionOfInterest = new Rect(ra.Width * 2 / 3, 0, ra.Width / 3, ra.Height), ReplaceDictionary = new Dictionary { ["渊下宫"] = ["渊下宮"], }, }); string minCountryLocalized = this.stringLocalizer.WithCultureGet(this.cultureInfo, areaName); Region? matchRect = list.OrderByDescending(r => r.Y).FirstOrDefault(r => r.Text.Contains(minCountryLocalized)); if (matchRect == null) { Logger.LogWarning("切换区域失败:{Country}", areaName); if (areaName == MapTypes.TheChasm.GetDescription() || areaName == MapTypes.Enkanomiya.GetDescription() || areaName == MapTypes.SeaOfBygoneEras.GetDescription() || areaName == MapTypes.AncientSacredMountain.GetDescription()) { throw new Exception($"切换独立地图区域[{areaName}]失败"); } } else { matchRect.Click(); Logger.LogInformation("切换到区域:{Country}", areaName); } await Delay(500, ct); } public async Task Tp(string name) { // 通过大地图传送到指定传送点 await Delay(500, ct); } public async Task TpByF1(string name) { // 传送到指定传送点 await Delay(500, ct); } public async Task ClickTpPoint(ImageRegion imageRegion) { // 1.判断是否在地图界面 if (!Bv.IsInBigMapUi(imageRegion)) throw new RetryException("不在地图界面"); // 2. 判断是否已经点出传送按钮 var hasTeleportButton = CheckTeleportButton(imageRegion); if (hasTeleportButton) return; // 可以传送了,结束 // 3. 没点出传送按钮,且不存在外部地图关闭按钮 // 说明只有两种可能,a. 点出来的是未激活传送点或者标点 b. 选择传送点选项列表 var mapCloseRa1 = imageRegion.Find(_assets.MapCloseButtonRo); if (!mapCloseRa1.IsEmpty()) throw new TpPointNotActivate("传送点未激活或不存在"); // 4. 循环判断选项列表是否有传送点(未激活点位也在里面) var hasMapChooseIcon = CheckMapChooseIcon(imageRegion); // 没有传送点说明不是传送点 if (!hasMapChooseIcon) throw new TpPointNotActivate("选项列表不存在传送点"); var teleportButtonFound = await NewRetry.WaitForElementAppear( _assets.TeleportButtonRo, () => { }, ct, 6, 300 ); if (!teleportButtonFound) throw new TpPointNotActivate("选项列表的传送点未激活"); await NewRetry.WaitForElementDisappear( _assets.TeleportButtonRo, screen => { screen.Find(_assets.TeleportButtonRo, ra => { ra.Click(); ra.Dispose(); }); }, ct, 6, 300 ); } private bool CheckTeleportButton(ImageRegion imageRegion) { var hasTeleportButton = false; imageRegion.Find(_assets.TeleportButtonRo, ra => { ra.Click(); hasTeleportButton = true; }); return hasTeleportButton; } /// /// 全匹配一遍并进行文字识别 /// 60ms ~200ms /// /// /// private bool CheckMapChooseIcon(ImageRegion imageRegion) { var hasMapChooseIcon = false; // 全匹配一遍 var rResultList = MatchTemplateHelper.MatchMultiPicForOnePic(imageRegion.CacheGreyMat[_assets.MapChooseIconRoi], _assets.MapChooseIconGreyMatList); // 按高度排序 if (rResultList.Count > 0) { rResultList = [.. rResultList.OrderBy(x => x.Y)]; // 点击最高的 foreach (var iconRect in rResultList) { // 200宽度的文字区域 using var ra = imageRegion.DeriveCrop(_assets.MapChooseIconRoi.X + iconRect.X + iconRect.Width, _assets.MapChooseIconRoi.Y + iconRect.Y - 8, 200, iconRect.Height + 16); using var textRegion = ra.Find(new RecognitionObject { // RecognitionType = RecognitionTypes.Ocr, RecognitionType = RecognitionTypes.ColorRangeAndOcr, LowerColor = new Scalar(249, 249, 249), // 只取白色文字 UpperColor = new Scalar(255, 255, 255), }); if (string.IsNullOrEmpty(textRegion.Text) || textRegion.Text.Length == 1) { continue; } Logger.LogInformation("传送:点击 {Option}", textRegion.Text.Replace(">", "")); var time = TaskContext.Instance().Config.QuickTeleportConfig.TeleportListClickDelay; time = time < 500 ? 500 : time; Thread.Sleep(time); ra.Click(); hasMapChooseIcon = true; break; } } return hasMapChooseIcon; } /// /// 给定的映射关系可以表示成 (x, y) 对的形式,其中 x 是输入值,y 是输出值 /// 1 - 1 /// 0.8 - 2 /// 0.6 - 3 /// 0.4 - 4 /// 0.2 - 5 /// 0 - 6 /// y=−5x+6 /// /// /// public double GetBigMapZoomLevel(ImageRegion region) { var s = Bv.GetBigMapScale(region); // 1~6 的缩放等级 return (-5 * s) + 6; } }