Files
better-genshin-impact/BetterGenshinImpact/GameTask/AutoTrackPath/TpTask.cs

1040 lines
41 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
/// <summary>
/// 传送任务
/// </summary>
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;
/// <summary>
/// 直接通过缩放比例按钮计算放大按钮的Y坐标
/// </summary>
private readonly int _zoomInButtonY = TaskContext.Instance().Config.TpConfig.ZoomStartY - 24; // y-coordinate for zoom-in button = _zoomStartY - 24
/// <summary>
/// 直接通过缩放比例按钮计算缩小按钮的Y坐标
/// </summary>
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;
}
/// <summary>
/// 传送到七天神像
/// </summary>
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);
}
/// <summary>
///
/// </summary>
/// <param name="tranX"> 传送后实际到达的点X坐标 </param>
/// <param name="tranY"> 传送后实际到达的点Y坐标 </param>
/// <param name="x"> 传送点 X 坐标 </param>
/// <param name="y"> 传送点 Y 坐标 </param>
/// <param name="d"> 期望最终离传送点的距离 </param>
/// <returns> </returns>
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);
}
/// <summary>
/// 获取离 x,y 最近的七天神像
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <returns></returns>
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("没找到最近的七天神像");
}
/// <summary>
///释放所有按键,并打开大地图界面
/// </summary>
/// <param name="retryCount">重试次数</param>
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;
}
}
}
}
/// <summary>
/// 通过大地图传送到指定坐标最近的传送点,然后移动到指定坐标
/// </summary>
/// <param name="tpX"></param>
/// <param name="tpY"></param>
/// <param name="mapName">独立地图名称</param>
/// <param name="force">强制以当前的tpX,tpY坐标进行自动传送</param>
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);
}
/// <summary>
/// 检查传送是否完成,未完成则等待
/// </summary>
/// <param name="maxAttempts">最大检查延时的次数</param>
/// <param name="delayMs">如果未完成加载,检查加载页面的延时。</param>
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("传送等待超时,换台电脑吧");
}
/// <summary>
/// 传送点是否在大地图窗口内
/// </summary>
/// <param name="mapName"></param>
/// <param name="bigMapInAllMapRect">大地图在整个游戏地图中的矩形位置(原神坐标系)</param>
/// <param name="x">传送点x坐标原神坐标系</param>
/// <param name="y">传送点y坐标原神坐标系</param>
/// <returns></returns>
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;
}
/// <summary>
/// 转换传送点坐标到窗体内需要点击的坐标
/// </summary>
/// <param name="mapName"></param>
/// <param name="bigMapInAllMapRect">大地图在整个游戏地图中的矩形位置(原神坐标系)</param>
/// <param name="x">传送点x坐标原神坐标系</param>
/// <param name="y">传送点y坐标原神坐标系</param>
/// <returns></returns>
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("打开大地图失败,请检查按键绑定中「打开地图」按键设置是否和原神游戏中一致!");
}
}
}
/// <summary>
/// 尝试打开地图界面
/// </summary>
private async Task<bool> 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("传送失败");
}
/// <summary>
/// 移动地图到指定传送点位置
/// 可能会移动不对,所以可以重试此方法
/// </summary>
/// <param name="x">目标x坐标</param>
/// <param name="y">目标y坐标</param>
/// <param name="mapName">地图名称</param>
/// <param name="finalZoomLevel">到达目标点的最小缩放等级,只在 MapZoomEnabled 为 True 生效</param>
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);
}
}
/// <summary>
/// 点击并移动鼠标
/// </summary>
/// <param name="x1">鼠标初始位置x</param>
/// <param name="y1">鼠标初始位置y</param>
/// <param name="x2">鼠标移动后位置x</param>
/// <param name="y2">鼠标移动后位置y</param>
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));
}
/// <summary>
/// 调整地图缩放级别以加速移动
/// </summary>
/// <param name="zoomIn">是否放大地图</param>
[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);
}
/// <summary>
/// 调整地图的缩放等级(整数缩放级别)。
/// </summary>
/// <param name="zoomLevel">目标等级1-6。整数。随着数字变大地图越小细节越少。</param>
[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);
}
}
/// <summary>
/// 将大地图缩放等级设置为指定值
/// </summary>
/// <remarks>
/// 缩放等级说明:
/// - 数值范围1.0(最大地图) 到 6.0(最小地图)
/// - 缩放效果:数值越大,地图显示范围越广,细节越少
/// - 缩放位置1.0 对应缩放条最上方6.0 对应缩放条最下方
/// - 推荐范围:建议在 2.0 到 5.0 之间调整,过大或过小可能影响操作
/// </remarks>
/// <param name="zoomLevel">当前缩放等级1.0-6.0,浮点数。</param>
/// <param name="targetZoomLevel">目标缩放等级1.0-6.0,浮点数。</param>
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("当前不在地图界面");
}
}
/// <summary>
/// 获取最接近的N个传送点坐标和所处区域
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="n">获取最近的 n 个传送点</param>
/// <returns></returns>
public List<GiTpPosition> 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<bool> 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, string[]>
{
["渊下宫"] = ["渊下宮"],
},
});
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;
}
/// <summary>
/// 全匹配一遍并进行文字识别
/// 60ms ~200ms
/// </summary>
/// <param name="imageRegion"></param>
/// <returns></returns>
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;
}
/// <summary>
/// 给定的映射关系可以表示成 (x, y) 对的形式,其中 x 是输入值y 是输出值
/// 1 - 1
/// 0.8 - 2
/// 0.6 - 3
/// 0.4 - 4
/// 0.2 - 5
/// 0 - 6
/// y=5x+6
/// </summary>
/// <param name="region"></param>
/// <returns></returns>
public double GetBigMapZoomLevel(ImageRegion region)
{
var s = Bv.GetBigMapScale(region);
// 1~6 的缩放等级
return (-5 * s) + 6;
}
}