From c9cd8fcd5fee19968cd067fce9f30dd39b9306eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BE=89=E9=B8=AD=E8=9B=8B?= Date: Tue, 24 Feb 2026 01:44:44 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=9C=B0=E5=9B=BE=E9=81=AE=E7=BD=A9):=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=B0=8F=E5=9C=B0=E5=9B=BE=E9=81=AE=E7=BD=A9?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E4=B8=8E=E8=87=AA=E5=8A=A8=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加小地图遮罩显示功能,在游戏主界面显示玩家位置及附近点位 - 新增自动记录路径功能开关配置 - 实现 MiniMapPointsCanvas 控件用于小地图点位渲染 - 扩展 MapMaskTrigger 以支持小地图遮罩和路径记录逻辑 --- .../GameTask/MapMask/MapMaskConfig.cs | 12 + .../GameTask/MapMask/MapMaskTrigger.cs | 36 +- .../View/Controls/MiniMapPointsCanvas.cs | 319 ++++++++++++++++++ BetterGenshinImpact/View/MaskWindow.xaml | 29 ++ BetterGenshinImpact/View/MaskWindow.xaml.cs | 32 ++ .../ViewModel/MaskWindowViewModel.cs | 6 + 6 files changed, 429 insertions(+), 5 deletions(-) create mode 100644 BetterGenshinImpact/View/Controls/MiniMapPointsCanvas.cs 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; } =