diff --git a/BetterGenshinImpact/GameTask/MapMask/MapMaskConfig.cs b/BetterGenshinImpact/GameTask/MapMask/MapMaskConfig.cs
index af3fa94f..28708cb1 100644
--- a/BetterGenshinImpact/GameTask/MapMask/MapMaskConfig.cs
+++ b/BetterGenshinImpact/GameTask/MapMask/MapMaskConfig.cs
@@ -15,6 +15,18 @@ public partial class MapMaskConfig : ObservableObject
///
[ObservableProperty]
private bool _enabled = true;
+
+ ///
+ /// 小地图遮罩是否启用
+ ///
+ [ObservableProperty]
+ private bool _miniMapMaskEnabled = true;
+
+ ///
+ /// 自动记录路径功能是否启用
+ ///
+ [ObservableProperty]
+ private bool _pathAutoRecordEnabled = true;
private MapPointApiProvider _mapPointApiProvider = MapPointApiProvider.MihoyoMap;
diff --git a/BetterGenshinImpact/GameTask/MapMask/MapMaskTrigger.cs b/BetterGenshinImpact/GameTask/MapMask/MapMaskTrigger.cs
index cd9a7f23..e0c2f44b 100644
--- a/BetterGenshinImpact/GameTask/MapMask/MapMaskTrigger.cs
+++ b/BetterGenshinImpact/GameTask/MapMask/MapMaskTrigger.cs
@@ -1,7 +1,8 @@
using System;
-using System.Windows;
using BetterGenshinImpact.Core.Recognition.OpenCv;
+using BetterGenshinImpact.GameTask.AutoPathing;
using BetterGenshinImpact.GameTask.Common.BgiVision;
+using BetterGenshinImpact.GameTask.Common.Element.Assets;
using BetterGenshinImpact.GameTask.Common.Map.Maps;
using BetterGenshinImpact.GameTask.Common.Map.Maps.Base;
using BetterGenshinImpact.GameTask.Common.Map.Maps.Layer;
@@ -9,6 +10,8 @@ using BetterGenshinImpact.Helpers;
using BetterGenshinImpact.View;
using BetterGenshinImpact.ViewModel;
using Microsoft.Extensions.Logging;
+using OpenCvSharp;
+using Rect = System.Windows.Rect;
namespace BetterGenshinImpact.GameTask.MapMask;
@@ -23,7 +26,7 @@ public class MapMaskTrigger : ITaskTrigger
public bool IsEnabled { get; set; }
public int Priority => 1; // 低优先级
public bool IsExclusive => false;
-
+
public GameUiCategory SupportedGameUiCategory => GameUiCategory.BigMap;
private readonly MapMaskConfig _config = TaskContext.Instance().Config.MapMaskConfig;
@@ -40,11 +43,13 @@ public class MapMaskTrigger : ITaskTrigger
private OpenCvSharp.Rect _prevRect = default;
private const int RectDebounceThreshold = 3;
+
+ private readonly NavigationInstance _navigationInstance = new();
public void Init()
{
IsEnabled = _config.Enabled;
-
+
// 关闭时隐藏UI
if (!IsEnabled)
{
@@ -108,8 +113,8 @@ public class MapMaskTrigger : ITaskTrigger
_prevRect = default;
return;
}
-
-
+
+
// if (_prevRect != default)
// {
// var dx = Math.Abs(rect256.X - _prevRect.X);
@@ -130,6 +135,27 @@ public class MapMaskTrigger : ITaskTrigger
}
else
{
+ if ((_config.MiniMapMaskEnabled || _config.PathAutoRecordEnabled) && Bv.IsInMainUi(region))
+ {
+ // 主界面上展示小地图
+ if (_config.MiniMapMaskEnabled)
+ {
+ var miniPoint = _navigationInstance.GetPositionStable(region, nameof(MapTypes.Teyvat), TaskContext.Instance().Config.PathingConditionConfig.MapMatchingMethod);
+ if (miniPoint != default)
+ {
+ UIDispatcherHelper.Invoke(() => { MaskWindow.Instance().MiniMapPointsCanvasControl.UpdateViewport(miniPoint.X, miniPoint.Y, 300, 300); });
+ }
+
+ // 自动记录路径
+ if (_config.PathAutoRecordEnabled)
+ {
+ // ...
+ }
+ }
+
+
+ }
+
_prevRect = default;
}
}
diff --git a/BetterGenshinImpact/View/Controls/MiniMapPointsCanvas.cs b/BetterGenshinImpact/View/Controls/MiniMapPointsCanvas.cs
new file mode 100644
index 00000000..3006a34b
--- /dev/null
+++ b/BetterGenshinImpact/View/Controls/MiniMapPointsCanvas.cs
@@ -0,0 +1,319 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Linq;
+using System.Threading;
+using System.Windows;
+using System.Windows.Media;
+using BetterGenshinImpact.Model.MaskMap;
+using BetterGenshinImpact.ViewModel;
+
+namespace BetterGenshinImpact.View.Controls;
+
+public sealed class MiniMapPointsCanvas : FrameworkElement
+{
+ private readonly VisualCollection _children;
+ private readonly DrawingVisual _drawingVisual;
+ private readonly Dictionary _colorBrushCache;
+ private int _refreshQueued;
+
+ private ObservableCollection? _points;
+ private List _allPoints = new();
+ private Dictionary _labelMap = new();
+ private Rect _viewportRect = Rect.Empty;
+
+ public MiniMapPointsCanvas()
+ {
+ _children = new VisualCollection(this);
+ _drawingVisual = new DrawingVisual();
+ _children.Add(_drawingVisual);
+ _colorBrushCache = new Dictionary();
+
+ IsHitTestVisible = false;
+
+ MapIconImageCache.ImageUpdated += PointImageCacheManagerOnImageUpdated;
+ }
+
+ protected override void OnVisualParentChanged(DependencyObject oldParent)
+ {
+ base.OnVisualParentChanged(oldParent);
+ if (VisualParent == null)
+ {
+ MapIconImageCache.ImageUpdated -= PointImageCacheManagerOnImageUpdated;
+ }
+ }
+
+ private void PointImageCacheManagerOnImageUpdated(object? sender, string e)
+ {
+ if (Interlocked.Exchange(ref _refreshQueued, 1) != 0)
+ {
+ return;
+ }
+
+ Dispatcher.BeginInvoke(() =>
+ {
+ Interlocked.Exchange(ref _refreshQueued, 0);
+ Refresh();
+ }, System.Windows.Threading.DispatcherPriority.Background);
+ }
+
+ protected override int VisualChildrenCount => _children.Count;
+
+ protected override Visual GetVisualChild(int index)
+ {
+ if (index < 0 || index >= _children.Count)
+ {
+ throw new ArgumentOutOfRangeException(nameof(index));
+ }
+
+ return _children[index];
+ }
+
+ private void OnPointsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
+ {
+ if (e.OldItems != null)
+ {
+ foreach (MaskMapPoint point in e.OldItems)
+ {
+ UnsubscribePoint(point);
+ }
+ }
+
+ if (e.NewItems != null)
+ {
+ foreach (MaskMapPoint point in e.NewItems)
+ {
+ SubscribePoint(point);
+ }
+ }
+
+ _allPoints = _points?.ToList() ?? new List();
+ Refresh();
+ }
+
+ private void SubscribePoint(MaskMapPoint point)
+ {
+ if (point is INotifyPropertyChanged notifyPoint)
+ {
+ notifyPoint.PropertyChanged += OnPointPropertyChanged;
+ }
+ }
+
+ private void UnsubscribePoint(MaskMapPoint point)
+ {
+ if (point is INotifyPropertyChanged notifyPoint)
+ {
+ notifyPoint.PropertyChanged -= OnPointPropertyChanged;
+ }
+ }
+
+ private void OnPointPropertyChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ Refresh();
+ }
+
+ private void RenderPoints()
+ {
+ using var dc = _drawingVisual.RenderOpen();
+ if (_allPoints.Count == 0 || _viewportRect.IsEmpty)
+ {
+ return;
+ }
+
+ var aw = ActualWidth;
+ var ah = ActualHeight;
+ if (aw <= 0 || ah <= 0)
+ {
+ return;
+ }
+
+ var side = Math.Min(aw, ah);
+ if (side <= 0)
+ {
+ return;
+ }
+
+ var clipRect = new Rect((aw - side) / 2.0, (ah - side) / 2.0, side, side);
+ var clip = new EllipseGeometry(clipRect);
+ dc.PushClip(clip);
+
+ var expandedViewport = _viewportRect;
+ expandedViewport.Inflate(MaskMapPointStatic.Width, MaskMapPointStatic.Height);
+
+ var scaleX = side / _viewportRect.Width;
+ var scaleY = side / _viewportRect.Height;
+
+ var pointSide = Math.Max(8, Math.Min(16, side / 12.0));
+
+ foreach (var point in _allPoints)
+ {
+ if (!expandedViewport.Contains(point.ImageX, point.ImageY))
+ {
+ continue;
+ }
+
+ var localX = clipRect.X + (point.ImageX - _viewportRect.X) * scaleX;
+ var localY = clipRect.Y + (point.ImageY - _viewportRect.Y) * scaleY;
+ DrawPoint(dc, point, localX, localY, pointSide, pointSide);
+ }
+
+ dc.Pop();
+ }
+
+ private void DrawPoint(DrawingContext dc, MaskMapPoint point, double centerX, double centerY, double width, double height)
+ {
+ var radius = width / 2.0;
+ const double strokeThickness = 2.0;
+
+ var circleCenter = new Point(centerX, centerY);
+
+ var fillBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#323947"));
+ fillBrush.Freeze();
+
+ var borderBrush = new SolidColorBrush(Color.FromRgb(0xD3, 0xBC, 0x8E));
+ borderBrush.Freeze();
+
+ var borderPen = new Pen(borderBrush, strokeThickness);
+ borderPen.Freeze();
+
+ var shadowBrush = new SolidColorBrush(Color.FromArgb(30, 0, 0, 0));
+ shadowBrush.Freeze();
+
+ var shadowOffset = new Point(2, 2);
+
+ var shadowCircleGeometry = new EllipseGeometry(
+ new Point(circleCenter.X + shadowOffset.X, circleCenter.Y + shadowOffset.Y),
+ radius, radius);
+ dc.DrawGeometry(shadowBrush, null, shadowCircleGeometry);
+
+ var circleGeometry = new EllipseGeometry(circleCenter, radius, radius);
+ dc.DrawGeometry(fillBrush, borderPen, circleGeometry);
+
+ if (_labelMap.TryGetValue(point.LabelId, out var label))
+ {
+ var image = MapIconImageCache.TryGet(label.IconUrl);
+ if (image != null)
+ {
+ var imageRect = new Rect(circleCenter.X - radius, circleCenter.Y - radius, width, height);
+ dc.PushClip(circleGeometry);
+ dc.DrawImage(image, imageRect);
+ dc.Pop();
+ }
+ else
+ {
+ _ = MapIconImageCache.GetAsync(label.IconUrl, CancellationToken.None);
+
+ var brush = GetColorBrush(label);
+ dc.DrawEllipse(brush, null, new Point(centerX, centerY), width / 2.0, height / 2.0);
+ }
+ }
+ else
+ {
+ var brush = new SolidColorBrush(GenerateRandomColor(point.Id));
+ brush.Freeze();
+ dc.DrawEllipse(brush, null, new Point(centerX, centerY), width / 2.0, height / 2.0);
+ }
+ }
+
+ private Brush GetColorBrush(MaskMapPointLabel label)
+ {
+ if (_colorBrushCache.TryGetValue(label.LabelId, out var cachedBrush))
+ {
+ return cachedBrush;
+ }
+
+ Color color;
+ if (label.Color.HasValue)
+ {
+ var c = label.Color.Value;
+ color = Color.FromArgb(c.A, c.R, c.G, c.B);
+ }
+ else
+ {
+ color = GenerateRandomColor(label.LabelId);
+ }
+
+ var brush = new SolidColorBrush(color);
+ brush.Freeze();
+ _colorBrushCache[label.LabelId] = brush;
+ return brush;
+ }
+
+ private static Color GenerateRandomColor(string seed)
+ {
+ var hash = seed?.GetHashCode() ?? 0;
+ var random = new Random(hash);
+ return Color.FromRgb(
+ (byte)random.Next(80, 256),
+ (byte)random.Next(80, 256),
+ (byte)random.Next(80, 256));
+ }
+
+ public void UpdatePoints(ObservableCollection points)
+ {
+ if (_points != null)
+ {
+ _points.CollectionChanged -= OnPointsCollectionChanged;
+ foreach (var point in _points)
+ {
+ UnsubscribePoint(point);
+ }
+ }
+
+ _points = points;
+
+ if (_points != null)
+ {
+ _points.CollectionChanged += OnPointsCollectionChanged;
+ foreach (var point in _points)
+ {
+ SubscribePoint(point);
+ }
+
+ _allPoints = _points.ToList();
+ }
+ else
+ {
+ _allPoints.Clear();
+ }
+
+ Refresh();
+ }
+
+ public void UpdateLabels(IEnumerable labels)
+ {
+ if (labels != null)
+ {
+ _labelMap = labels.ToDictionary(l => l.LabelId, l => l);
+ _colorBrushCache.Clear();
+ }
+ else
+ {
+ _labelMap.Clear();
+ _colorBrushCache.Clear();
+ }
+
+ Refresh();
+ }
+
+ public void UpdateViewport(double x, double y, double width, double height)
+ {
+ var newRect = new Rect(x, y, width, height);
+ if (newRect.Equals(_viewportRect))
+ {
+ return;
+ }
+
+ _viewportRect = newRect;
+ Refresh();
+ }
+
+ public void Refresh()
+ {
+ RenderPoints();
+ }
+}
+
diff --git a/BetterGenshinImpact/View/MaskWindow.xaml b/BetterGenshinImpact/View/MaskWindow.xaml
index 5701c577..3437cd74 100644
--- a/BetterGenshinImpact/View/MaskWindow.xaml
+++ b/BetterGenshinImpact/View/MaskWindow.xaml
@@ -86,6 +86,35 @@
Grid.Column="0"
Grid.ColumnSpan="6"
ClipToBounds="True">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
>(this, (sender, msg) =>
+ {
+ if (msg.PropertyName != "SendCurrentPosition")
+ {
+ return;
+ }
+
+ if (msg.NewValue is not CvPoint2f pos)
+ {
+ return;
+ }
+
+ if (pos.X <= 0 || pos.Y <= 0)
+ {
+ return;
+ }
+
+ _miniMapLastPosition = pos;
+ const double viewportSize = 512;
+ MiniMapPointsCanvasControl.UpdateViewport(pos.X - viewportSize / 2.0, pos.Y - viewportSize / 2.0, viewportSize, viewportSize);
+ });
}
private void PointsCanvasControlOnViewportChanged(object? sender, EventArgs e)
@@ -215,6 +243,8 @@ public partial class MaskWindow : Window
IsVisibleChanged -= MaskWindowOnIsVisibleChanged;
StateChanged -= MaskWindowOnStateChanged;
+ WeakReferenceMessenger.Default.Unregister>(this);
+
if (_maskWindowConfig != null)
{
_maskWindowConfig.PropertyChanged -= MaskWindowConfigOnPropertyChanged;
@@ -265,6 +295,7 @@ public partial class MaskWindow : Window
if (_viewModel != null)
{
PointsCanvasControl.UpdateLabels(_viewModel.MapPointLabels);
+ MiniMapPointsCanvasControl.UpdateLabels(_viewModel.MapPointLabels);
}
});
}
@@ -276,6 +307,7 @@ public partial class MaskWindow : Window
if (_viewModel != null)
{
PointsCanvasControl.UpdatePoints(_viewModel.MapPoints);
+ MiniMapPointsCanvasControl.UpdatePoints(_viewModel.MapPoints);
}
});
}
diff --git a/BetterGenshinImpact/ViewModel/MaskWindowViewModel.cs b/BetterGenshinImpact/ViewModel/MaskWindowViewModel.cs
index 8ebe6024..184a37f9 100644
--- a/BetterGenshinImpact/ViewModel/MaskWindowViewModel.cs
+++ b/BetterGenshinImpact/ViewModel/MaskWindowViewModel.cs
@@ -78,6 +78,12 @@ namespace BetterGenshinImpact.ViewModel
[ObservableProperty] private string _mapPointsLoadingText = "正在加载点位...";
+ public double MiniMapOverlayLeftRatio => 67d / 1920d;
+
+ public double MiniMapOverlayTopRatio => 21d / 1080d;
+
+ public double MiniMapOverlaySizeRatio => 200d / 1080d;
+
public sealed record MapPointApiProviderOption(MapPointApiProvider Provider, string DisplayName);
public IReadOnlyList MapPointApiProviderOptions { get; } =