From 95e0f0175313cca7a7dd55fcb0a9c811a5deca6c 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 15:34:50 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=B0=8F=E5=9C=B0=E5=9B=BE?= =?UTF-8?q?=E9=81=AE=E7=BD=A9=EF=BC=8C=E5=9C=A8=E5=B0=8F=E5=9C=B0=E5=9B=BE?= =?UTF-8?q?=E4=B8=8A=E5=B1=95=E7=A4=BA=E8=B5=84=E6=BA=90=E7=82=B9=E4=BD=8D?= =?UTF-8?q?=20(#2830)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Common/Element/Assets/MapAssets.cs | 3 + .../GameTask/MapMask/MapMaskConfig.cs | 12 + .../GameTask/MapMask/MapMaskTrigger.cs | 49 ++- BetterGenshinImpact/Helpers/DpiHelper.cs | 12 +- .../Model/MaskMap/MaskMapPoint.cs | 4 +- .../View/Controls/MiniMapPointsCanvas.cs | 355 ++++++++++++++++++ .../View/Controls/PointsCanvas.cs | 44 ++- BetterGenshinImpact/View/MaskWindow.xaml | 33 ++ BetterGenshinImpact/View/MaskWindow.xaml.cs | 26 -- .../View/Pages/TriggerSettingsPage.xaml | 49 ++- .../ViewModel/MaskWindowViewModel.cs | 15 +- 11 files changed, 549 insertions(+), 53 deletions(-) create mode 100644 BetterGenshinImpact/View/Controls/MiniMapPointsCanvas.cs diff --git a/BetterGenshinImpact/GameTask/Common/Element/Assets/MapAssets.cs b/BetterGenshinImpact/GameTask/Common/Element/Assets/MapAssets.cs index 3c34b36c..1e6d768a 100644 --- a/BetterGenshinImpact/GameTask/Common/Element/Assets/MapAssets.cs +++ b/BetterGenshinImpact/GameTask/Common/Element/Assets/MapAssets.cs @@ -14,6 +14,9 @@ namespace BetterGenshinImpact.GameTask.Common.Element.Assets; public class MapAssets : BaseAssets { public Rect MimiMapRect { get; } + + public static Rect MimiMapRect1080P = new Rect(62, 19,212,212); + public MapAssets() { diff --git a/BetterGenshinImpact/GameTask/MapMask/MapMaskConfig.cs b/BetterGenshinImpact/GameTask/MapMask/MapMaskConfig.cs index af3fa94f..4f9cc640 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 = false; + + /// + /// 自动记录路径功能是否启用 + /// + [ObservableProperty] + private bool _pathAutoRecordEnabled = false; private MapPointApiProvider _mapPointApiProvider = MapPointApiProvider.MihoyoMap; diff --git a/BetterGenshinImpact/GameTask/MapMask/MapMaskTrigger.cs b/BetterGenshinImpact/GameTask/MapMask/MapMaskTrigger.cs index cd9a7f23..8f21ee61 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,8 +26,8 @@ public class MapMaskTrigger : ITaskTrigger public bool IsEnabled { get; set; } public int Priority => 1; // 低优先级 public bool IsExclusive => false; - - public GameUiCategory SupportedGameUiCategory => GameUiCategory.BigMap; + + public GameUiCategory SupportedGameUiCategory => GameUiCategory.Unknown; private readonly MapMaskConfig _config = TaskContext.Instance().Config.MapMaskConfig; private readonly string _mapMatchingMethod = TaskContext.Instance().Config.PathingConditionConfig.MapMatchingMethod; @@ -41,10 +44,12 @@ public class MapMaskTrigger : ITaskTrigger 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,38 @@ 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) + { + // 展示窗口是 212 + double viewportSize = MapAssets.MimiMapRect1080P.Width / 3.0 * 10; + UIDispatcherHelper.Invoke(() => + { + MaskWindow.Instance().MiniMapPointsCanvasControl.UpdateViewport( + miniPoint.X - viewportSize / 2.0, + miniPoint.Y - viewportSize / 2.0, + viewportSize, + viewportSize); + }); + } + else + { + UIDispatcherHelper.Invoke(() => { MaskWindow.Instance().MiniMapPointsCanvasControl.UpdateViewport(0, 0, 0, 0); }); + } + + // 自动记录路径 + if (_config.PathAutoRecordEnabled) + { + // ... + } + } + } + _prevRect = default; } } diff --git a/BetterGenshinImpact/Helpers/DpiHelper.cs b/BetterGenshinImpact/Helpers/DpiHelper.cs index 7abd2afb..58226bf7 100644 --- a/BetterGenshinImpact/Helpers/DpiHelper.cs +++ b/BetterGenshinImpact/Helpers/DpiHelper.cs @@ -2,6 +2,7 @@ using System.Diagnostics; using System.Windows; using System.Windows.Interop; +using BetterGenshinImpact.GameTask; using Vanara.PInvoke; namespace BetterGenshinImpact.Helpers; @@ -18,7 +19,16 @@ public class DpiHelper if (Environment.OSVersion.Version >= new Version(6, 3) && UIDispatcherHelper.MainWindow != null) { - HWND hWnd = new WindowInteropHelper(Application.Current?.MainWindow).Handle; + HWND hWnd = HWND.NULL; + if (TaskContext.Instance().IsInitialized) + { + hWnd = TaskContext.Instance().GameHandle; + } + else + { + hWnd = new WindowInteropHelper(Application.Current?.MainWindow).Handle; + } + HMONITOR hMonitor = User32.MonitorFromWindow(hWnd, User32.MonitorFlags.MONITOR_DEFAULTTONEAREST); SHCore.GetDpiForMonitor(hMonitor, SHCore.MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out _, out uint dpiY); return dpiY / 96f; diff --git a/BetterGenshinImpact/Model/MaskMap/MaskMapPoint.cs b/BetterGenshinImpact/Model/MaskMap/MaskMapPoint.cs index 773439b1..76b95352 100644 --- a/BetterGenshinImpact/Model/MaskMap/MaskMapPoint.cs +++ b/BetterGenshinImpact/Model/MaskMap/MaskMapPoint.cs @@ -19,12 +19,12 @@ public class MaskMapPoint public double GameY { get; set; } /// - /// 游戏图像地图的坐标 X + /// 2048级别游戏图像地图的坐标 X /// public double ImageX { get; set; } /// - /// 游戏图像地图的坐标 Y + /// 2048级别游戏图像地图的坐标 Y /// public double ImageY { get; set; } diff --git a/BetterGenshinImpact/View/Controls/MiniMapPointsCanvas.cs b/BetterGenshinImpact/View/Controls/MiniMapPointsCanvas.cs new file mode 100644 index 00000000..a7ded614 --- /dev/null +++ b/BetterGenshinImpact/View/Controls/MiniMapPointsCanvas.cs @@ -0,0 +1,355 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; +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 +{ + public static readonly DependencyProperty PointsSourceProperty = + DependencyProperty.Register( + nameof(PointsSource), + typeof(ObservableCollection), + typeof(MiniMapPointsCanvas), + new PropertyMetadata(null, OnPointsSourceChanged)); + + public static readonly DependencyProperty LabelsSourceProperty = + DependencyProperty.Register( + nameof(LabelsSource), + typeof(IEnumerable), + typeof(MiniMapPointsCanvas), + new PropertyMetadata(null, OnLabelsSourceChanged)); + + 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 ObservableCollection? PointsSource + { + get => (ObservableCollection?)GetValue(PointsSourceProperty); + set => SetValue(PointsSourceProperty, value); + } + + public IEnumerable? LabelsSource + { + get => (IEnumerable?)GetValue(LabelsSourceProperty); + set => SetValue(LabelsSourceProperty, value); + } + + public MiniMapPointsCanvas() + { + _children = new VisualCollection(this); + _drawingVisual = new DrawingVisual(); + _children.Add(_drawingVisual); + _colorBrushCache = new Dictionary(); + + IsHitTestVisible = false; + + MapIconImageCache.ImageUpdated += PointImageCacheManagerOnImageUpdated; + } + + private static void OnPointsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var canvas = (MiniMapPointsCanvas)d; + canvas.UpdatePoints(e.NewValue as ObservableCollection); + } + + private static void OnLabelsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var canvas = (MiniMapPointsCanvas)d; + canvas.UpdateLabels(e.NewValue as IEnumerable); + } + + 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 || _viewportRect.Width == 0) + { + 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/Controls/PointsCanvas.cs b/BetterGenshinImpact/View/Controls/PointsCanvas.cs index f82ed17f..721e62b9 100644 --- a/BetterGenshinImpact/View/Controls/PointsCanvas.cs +++ b/BetterGenshinImpact/View/Controls/PointsCanvas.cs @@ -28,7 +28,7 @@ public class PointsCanvas : FrameworkElement private int _refreshQueued; // 私有字段 - private ObservableCollection _points; + private ObservableCollection? _points; private List _allPoints = new(); private Dictionary _labelMap = new(); private Rect _viewportRect = Rect.Empty; @@ -37,6 +37,20 @@ public class PointsCanvas : FrameworkElement #region 依赖属性 + public static readonly DependencyProperty PointsSourceProperty = + DependencyProperty.Register( + nameof(PointsSource), + typeof(ObservableCollection), + typeof(PointsCanvas), + new PropertyMetadata(null, OnPointsSourceChanged)); + + public static readonly DependencyProperty LabelsSourceProperty = + DependencyProperty.Register( + nameof(LabelsSource), + typeof(IEnumerable), + typeof(PointsCanvas), + new PropertyMetadata(null, OnLabelsSourceChanged)); + public static readonly DependencyProperty PointClickCommandProperty = DependencyProperty.Register( nameof(PointClickCommand), @@ -67,6 +81,18 @@ public class PointsCanvas : FrameworkElement set => SetValue(PointClickCommandProperty, value); } + public ObservableCollection? PointsSource + { + get => (ObservableCollection?)GetValue(PointsSourceProperty); + set => SetValue(PointsSourceProperty, value); + } + + public IEnumerable? LabelsSource + { + get => (IEnumerable?)GetValue(LabelsSourceProperty); + set => SetValue(LabelsSourceProperty, value); + } + /// /// 右键点击命令 /// @@ -87,6 +113,18 @@ public class PointsCanvas : FrameworkElement #endregion + private static void OnPointsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var canvas = (PointsCanvas)d; + canvas.UpdatePoints(e.NewValue as ObservableCollection); + } + + private static void OnLabelsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var canvas = (PointsCanvas)d; + canvas.UpdateLabels(e.NewValue as IEnumerable); + } + public PointsCanvas() { _children = new VisualCollection(this); @@ -508,7 +546,7 @@ public class PointsCanvas : FrameworkElement /// /// 更新点位数据 /// - public void UpdatePoints(ObservableCollection points) + public void UpdatePoints(ObservableCollection? points) { // 取消订阅旧集合 if (_points != null) @@ -541,7 +579,7 @@ public class PointsCanvas : FrameworkElement /// /// 更新标签数据 /// - public void UpdateLabels(IEnumerable labels) + public void UpdateLabels(IEnumerable? labels) { if (labels != null) { diff --git a/BetterGenshinImpact/View/MaskWindow.xaml b/BetterGenshinImpact/View/MaskWindow.xaml index 5701c577..c1d9b363 100644 --- a/BetterGenshinImpact/View/MaskWindow.xaml +++ b/BetterGenshinImpact/View/MaskWindow.xaml @@ -86,6 +86,37 @@ Grid.Column="0" Grid.ColumnSpan="6" ClipToBounds="True"> + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BetterGenshinImpact/View/MaskWindow.xaml.cs b/BetterGenshinImpact/View/MaskWindow.xaml.cs index 26d3c830..9794db2e 100644 --- a/BetterGenshinImpact/View/MaskWindow.xaml.cs +++ b/BetterGenshinImpact/View/MaskWindow.xaml.cs @@ -191,11 +191,6 @@ public partial class MaskWindow : Window RefreshPosition(); PrintSystemInfo(); - if (_viewModel != null) - { - PointsCanvasControl.UpdateLabels(_viewModel.MapPointLabels); - PointsCanvasControl.UpdatePoints(_viewModel.MapPoints); - } PointsCanvasControl.ViewportChanged += PointsCanvasControlOnViewportChanged; } @@ -258,27 +253,6 @@ public partial class MaskWindow : Window } } - if (e.PropertyName == nameof(MaskWindowViewModel.MapPointLabels)) - { - Dispatcher.Invoke(() => - { - if (_viewModel != null) - { - PointsCanvasControl.UpdateLabels(_viewModel.MapPointLabels); - } - }); - } - - if (e.PropertyName == nameof(MaskWindowViewModel.MapPoints)) - { - Dispatcher.Invoke(() => - { - if (_viewModel != null) - { - PointsCanvasControl.UpdatePoints(_viewModel.MapPoints); - } - }); - } } private void UpdateClickThroughState() diff --git a/BetterGenshinImpact/View/Pages/TriggerSettingsPage.xaml b/BetterGenshinImpact/View/Pages/TriggerSettingsPage.xaml index 172fad45..4b2b6ba5 100644 --- a/BetterGenshinImpact/View/Pages/TriggerSettingsPage.xaml +++ b/BetterGenshinImpact/View/Pages/TriggerSettingsPage.xaml @@ -975,16 +975,20 @@ - - + + - - + + + + + + 在遮罩窗口中显示大地图位置与标点信息 + - - - + + + + + + + + + + + + + + + + + MapAssets.MimiMapRect1080P.X / 1920d; + + public double MiniMapOverlayTopRatio => MapAssets.MimiMapRect1080P.Y / 1080d; + + public double MiniMapOverlaySizeRatio => MapAssets.MimiMapRect1080P.Width / 1080d; + public sealed record MapPointApiProviderOption(MapPointApiProvider Provider, string DisplayName); public IReadOnlyList MapPointApiProviderOptions { get; } =