diff --git a/BetterGenshinImpact/GameTask/AutoTrackPath/TpTask.cs b/BetterGenshinImpact/GameTask/AutoTrackPath/TpTask.cs index 748a7648..8e5ea498 100644 --- a/BetterGenshinImpact/GameTask/AutoTrackPath/TpTask.cs +++ b/BetterGenshinImpact/GameTask/AutoTrackPath/TpTask.cs @@ -894,7 +894,7 @@ public class TpTask if (matchRect == null) { Logger.LogWarning("切换区域失败:{Country}", areaName); - if (areaName == MapTypes.TheChasm.GetDescription() || areaName == MapTypes.Enkanomiya.GetDescription()) + if (areaName == MapTypes.TheChasm.GetDescription() || areaName == MapTypes.Enkanomiya.GetDescription() || areaName == MapTypes.SeaOfBygoneEras.GetDescription()) { throw new Exception($"切换独立地图区域[{areaName}]失败"); } diff --git a/BetterGenshinImpact/GameTask/Common/Map/Maps/Base/DisplayMapTypes.cs b/BetterGenshinImpact/GameTask/Common/Map/Maps/Base/DisplayMapTypes.cs index d8978d11..98b5cb3f 100644 --- a/BetterGenshinImpact/GameTask/Common/Map/Maps/Base/DisplayMapTypes.cs +++ b/BetterGenshinImpact/GameTask/Common/Map/Maps/Base/DisplayMapTypes.cs @@ -13,4 +13,7 @@ public enum DisplayMapTypes [Description("渊下宫")] Enkanomiya, + + [Description("旧日之海")] + SeaOfBygoneEras, } \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/Common/Map/Maps/SeaOfBygoneErasMap.cs b/BetterGenshinImpact/GameTask/Common/Map/Maps/SeaOfBygoneErasMap.cs index 7abd52d6..b1fceb8a 100644 --- a/BetterGenshinImpact/GameTask/Common/Map/Maps/SeaOfBygoneErasMap.cs +++ b/BetterGenshinImpact/GameTask/Common/Map/Maps/SeaOfBygoneErasMap.cs @@ -1,26 +1,47 @@ -using BetterGenshinImpact.GameTask.Common.Map.Maps.Base; +using BetterGenshinImpact.Core.Config; +using BetterGenshinImpact.GameTask.Common.Map.Maps.Base; using OpenCvSharp; +using Microsoft.Extensions.Logging; +using BetterGenshinImpact.Core.Recognition.OpenCv; +using System.Collections.Generic; +using System.Linq; +using System; +using Newtonsoft.Json.Linq; + namespace BetterGenshinImpact.GameTask.Common.Map.Maps; /// /// 旧日之海 -/// 从3x4改成了1x2 -/// 大地图都是半黑的,传送可能有问题 /// public class SeaOfBygoneErasMap : SceneBaseMap { #region 地图参数 - static readonly int GameMapRows = 1; // 游戏坐标下地图块的行数 - static readonly int GameMapCols = 2; // 游戏坐标下地图块的列数 - static readonly int GameMapUpRows = 0; // 游戏坐标下 左上角离地图原点的行数(注意原点在块的右下角) TODO 没找到 - static readonly int GameMapLeftCols = 0; // 游戏坐标下 左上角离地图原点的列数(注意原点在块的右下角) TODO 没找到 + static readonly int GameMapRows = 3; // 游戏坐标下地图块的行数 + static readonly int GameMapCols = 4; // 游戏坐标下地图块的列数 + static readonly int GameMapUpRows = 2; // 游戏坐标下 左上角离地图原点的行数(注意原点在块的右下角) + static readonly int GameMapLeftCols = 5; // 游戏坐标下 左上角离地图原点的列数(注意原点在块的右下角) #endregion 地图参数 static readonly int SeaOfBygoneErasMapImageBlockWidth = 1024; + private static Mat TeleportTemplate; + private static Mat TeleportTemplateMask; + private List MapTeleports; + + static SeaOfBygoneErasMap() + { + Mat img = GameTaskManager.LoadAssetImage("QuickTeleport", "TeleportTransparentBackground.png", ImreadModes.Unchanged); + + TeleportTemplate = new Mat(); + Cv2.CvtColor(img, TeleportTemplate, ColorConversionCodes.BGRA2GRAY); + + Mat[] channels = Cv2.Split(img); + TeleportTemplateMask = channels[3]; + } + public SeaOfBygoneErasMap() : base(type: MapTypes.SeaOfBygoneEras, mapSize: new Size(GameMapCols * SeaOfBygoneErasMapImageBlockWidth, GameMapRows * SeaOfBygoneErasMapImageBlockWidth), mapOriginInImageCoordinate: new Point2f((GameMapLeftCols + 1) * SeaOfBygoneErasMapImageBlockWidth, (GameMapUpRows + 1) * SeaOfBygoneErasMapImageBlockWidth), @@ -28,7 +49,222 @@ public class SeaOfBygoneErasMap : SceneBaseMap splitRow: 0, splitCol: 0) { + ExtractAndSaveFeature(Global.Absolute("Assets/Map/SeaOfBygoneEras/SeaOfBygoneEras_0_1024.png")); Layers = BaseMapLayer.LoadLayers(this); + + var mapTeleports = new List(); + var tpJson = System.IO.File.ReadAllText(Global.Absolute(@"GameTask\AutoTrackPath\Assets\tp.json")); + + JArray j = JArray.Parse(tpJson); + foreach (JObject i in j) + { + var sceneId = i["sceneId"]; + if (sceneId != null && (int)sceneId == 11) + { + foreach (var p in i["points"]!) + { + if ((string)p["type"]! != "Teleport") + { + continue; + } + var x = (float)p["position"]![2]!; + var y = (float)p["position"]![0]!; + var (x1, y1) = ConvertGenshinMapCoordinatesToImageCoordinates(x, y); + mapTeleports.Add(new Point(x1, y1)); + } + } + } + mapTeleports = mapTeleports.OrderBy(i => i.X).ThenBy(i => i.Y).ToList(); + MapTeleports = mapTeleports; + } + + public override Point2f GetBigMapPosition(Mat greyBigMapMat) + { + var rect = GetBigMapRectByTeleports(greyBigMapMat); + if (rect != default) + { + return rect.GetCenterPoint(); + } + + return base.GetBigMapPosition(greyBigMapMat); + } + + public override Rect GetBigMapRect(Mat greyBigMapMat) + { + var rect = GetBigMapRectByTeleports(greyBigMapMat); + if (rect != default) + { + return rect; + } + + return base.GetBigMapRect(greyBigMapMat); + } + + private Rect GetBigMapRectByTeleports(Mat greyBigMapMat) + { + // It's fine to miss some, but definitely no false positive results. + const double threshold = 0.99; + using Mat result = new Mat(); + Cv2.MatchTemplate(greyBigMapMat, TeleportTemplate, result, TemplateMatchModes.CCorrNormed, TeleportTemplateMask); + + var teleportPoints = new List(); + + // Step 1: Get all teleport positions from current screenshot + for (int i = 1; i < result.Rows - 1; ++i) + { + for (int j = 1; j < result.Cols - 1; ++j) + { + float val = result.At(i, j); + + if (val > threshold) + { + if (val >= result.At(i - 1, j - 1) && + val >= result.At(i - 1, j) && + val >= result.At(i - 1, j + 1) && + val >= result.At(i, j - 1) && + val >= result.At(i, j + 1) && + val >= result.At(i + 1, j - 1) && + val >= result.At(i + 1, j) && + val >= result.At(i + 1, j + 1)) + { + var newPoint = new Point(j, i); + bool tooClose = false; + foreach (var p in teleportPoints) + { + if (p.DistanceTo(newPoint) < 50) + { + tooClose = true; + break; + } + } + if (!tooClose) + { + teleportPoints.Add(newPoint); + } + } + } + } + } + + if (teleportPoints.Count < 2) + { + return default; + } + teleportPoints = teleportPoints.OrderBy(i => i.X).ThenBy(i => i.Y).ToList(); + /* + foreach (var p in teleportPoints) { + Logger.LogInformation("Teleport point: {a}", p); + } + Logger.LogInformation("Total telepoints: {c}", teleportPoints.Count); + */ + + Func GetAngleOfTwoPoints = (p0, p1) => + { + double deltaX = p0.X - p1.X; + int deltaY = p0.Y - p1.Y; + if (deltaY == 0) { return 90; } + var val = Math.Atan(deltaX / deltaY); + return val / Math.PI * 180; + }; + + // Step 2: find a diagonal determined by two of the teleports, let's call these two teleports reference points + Point rp0 = new Point(); + Point rp1 = new Point(); + double refAngle = 0.0; + { + double minAngleDiff = 180; + for (int i = 0; i < teleportPoints.Count; ++i) + { + for (int j = i + 1; j < teleportPoints.Count; ++j) + { + var p0 = teleportPoints[i]; + var p1 = teleportPoints[j]; + var angle = GetAngleOfTwoPoints(p0, p1); + if (Math.Abs(Math.Abs(angle) - 45) < minAngleDiff) + { + rp0 = p0; + rp1 = p1; + minAngleDiff = Math.Abs(Math.Abs(angle) - 45); + } + } + } + refAngle = GetAngleOfTwoPoints(rp0, rp1); + // Logger.LogInformation("Reference points {a} and {b}, Angle {c}", rp0, rp1, refAngle); + } + + { + /* + var debugMat = new Mat(); + Cv2.CvtColor(greyBigMapMat, debugMat, ColorConversionCodes.GRAY2BGR); + foreach (var p in teleportPoints) + { + Cv2.DrawMarker(debugMat, p, new Scalar(255, 0, 0), MarkerTypes.Cross, 20, 2); + } + Cv2.DrawMarker(debugMat, rp0, new Scalar(0, 255, 0), MarkerTypes.TriangleUp, 20, 2); + Cv2.DrawMarker(debugMat, rp1, new Scalar(0, 255, 0), MarkerTypes.TriangleUp, 20, 2); + Cv2.ImWrite(((DateTimeOffset)DateTime.UtcNow).ToUnixTimeMilliseconds().ToString() + ".png", debugMat); + */ + } + + // Step 3: For all diagonals determined by pairs of teleports on this map. + var minDeviation = double.MaxValue; + var transformParamScale = 0.0; + var transformParamDeltaX = 0.0; + var transformParamDeltaY = 0.0; + for (int i = 0; i < MapTeleports.Count; ++i) + { + for (int j = i + 1; j < MapTeleports.Count; ++j) + { + var mp0 = MapTeleports[i]; + var mp1 = MapTeleports[j]; + var angle = GetAngleOfTwoPoints(mp0, mp1); + if (Math.Abs(angle - refAngle) < 5) + { + // Step 4: Assuming this pair corresponds to the reference points + var mpDist = mp0.DistanceTo(mp1); + var rpDist = rp0.DistanceTo(rp1); + var scale = mpDist / rpDist; + var deltaX = mp0.X - rp0.X * scale; + var deltaY = mp0.Y - rp0.Y * scale; + + Func transformPoint = i => + { + return new Point(i.X * scale + deltaX, i.Y * scale + deltaY); + }; + + var transformedPoints = teleportPoints.Select(i => transformPoint(i)).ToList(); + // Step 5: Check how close the fit is + double totalDeviation = 0; + foreach (var p in transformedPoints) + { + var minDist = MapTeleports.Select(i => i.DistanceTo(p)).Min(); + totalDeviation += minDist; + } + if (totalDeviation < minDeviation) + { + minDeviation = totalDeviation; + transformParamScale = scale; + transformParamDeltaX = deltaX; + transformParamDeltaY = deltaY; + } + } + } + } + // Logger.LogInformation("Min deviation: {d}", minDeviation); + if (minDeviation < 200) + { + Func transformPoint = i => + { + return new Point(i.X * transformParamScale + transformParamDeltaX, i.Y * transformParamScale + transformParamDeltaY); + }; + var pTopLeft = transformPoint(new Point(0, 0)); + var pBottomRight = transformPoint(new Point(greyBigMapMat.Width, greyBigMapMat.Height)); + // Logger.LogInformation("Rect: {a}, {b}", pTopLeft, pBottomRight); + return new Rect(pTopLeft.X, pTopLeft.Y, pBottomRight.X - pTopLeft.X, pBottomRight.Y - pTopLeft.Y); + } + + + return default; } } \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/QuickTeleport/Assets/1920x1080/TeleportTransparentBackground.png b/BetterGenshinImpact/GameTask/QuickTeleport/Assets/1920x1080/TeleportTransparentBackground.png new file mode 100644 index 00000000..b0bf883d Binary files /dev/null and b/BetterGenshinImpact/GameTask/QuickTeleport/Assets/1920x1080/TeleportTransparentBackground.png differ diff --git a/BetterGenshinImpact/ViewModel/Windows/MapViewerViewModel.cs b/BetterGenshinImpact/ViewModel/Windows/MapViewerViewModel.cs index 20605a68..a5bd8fe6 100644 --- a/BetterGenshinImpact/ViewModel/Windows/MapViewerViewModel.cs +++ b/BetterGenshinImpact/ViewModel/Windows/MapViewerViewModel.cs @@ -98,6 +98,10 @@ public partial class MapViewerViewModel : ObservableObject { _mapImage = new Mat(Global.Absolute(@"Assets/Map/Enkanomiya/Enkanomiya_0_1024.png")); } + else if (mapName == MapTypes.SeaOfBygoneEras.ToString()) + { + _mapImage = new Mat(Global.Absolute(@"Assets/Map/SeaOfBygoneEras/SeaOfBygoneEras_0_1024.png")); + } else { throw new Exception("暂时不支持展示路径的地图类型:" + mapName);