using BetterGenshinImpact.Core.Config;
using BetterGenshinImpact.Core.Recognition.OpenCv;
using BetterGenshinImpact.GameTask;
using BetterGenshinImpact.Genshin.Settings;
using BetterGenshinImpact.Helpers;
using BetterGenshinImpact.Helpers.DpiAwareness;
using BetterGenshinImpact.View.Drawable;
using Microsoft.Extensions.Logging;
using Serilog.Sinks.RichTextBox.Abstraction;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Interop;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
using BetterGenshinImpact.Genshin.Settings2;
using BetterGenshinImpact.Model.MaskMap;
using BetterGenshinImpact.ViewModel;
using BetterGenshinImpact.View.Windows;
using Vanara.PInvoke;
using FontFamily = System.Windows.Media.FontFamily;
namespace BetterGenshinImpact.View;
///
/// 一个用于覆盖在游戏窗口上的窗口,用于显示识别结果、显示日志、设置区域位置等
/// 请使用 Instance 方法获取单例
///
public partial class MaskWindow : Window
{
private static MaskWindow? _maskWindow;
private static readonly Typeface _typeface;
private static readonly Typeface _fgiTypeface;
private MaskWindowViewModel? _viewModel;
private IRichTextBox? _richTextBox;
private readonly ILogger _logger = App.GetLogger();
private MaskWindowConfig? _maskWindowConfig;
private MapLabelSearchWindow? _mapLabelSearchWindow;
private CancellationTokenSource? _mapLabelCategorySelectCts;
static MaskWindow()
{
if (Application.Current.TryFindResource("TextThemeFontFamily") is FontFamily fontFamily)
{
_typeface = fontFamily.GetTypefaces().First();
}
else
{
_typeface = new FontFamily("Microsoft Yahei UI").GetTypefaces().First();
}
try
{
_fgiTypeface = new FontFamily(new Uri("pack://application:,,,/"), "./Resources/Fonts/Fgi-Regular.ttf#Fgi-Regular").GetTypefaces().First();
}
catch
{
_fgiTypeface = _typeface;
}
DefaultStyleKeyProperty.OverrideMetadata(typeof(MaskWindow), new FrameworkPropertyMetadata(typeof(MaskWindow)));
}
public static MaskWindow Instance()
{
if (_maskWindow == null)
{
throw new Exception("MaskWindow 未初始化");
}
return _maskWindow;
}
public static MaskWindow? InstanceNullable()
{
return _maskWindow;
}
public bool IsExist()
{
return _maskWindow != null && PresentationSource.FromVisual(_maskWindow) != null;
}
public void BringToTop()
{
User32.BringWindowToTop(new WindowInteropHelper(this).Handle);
}
public void RefreshPosition()
{
RefreshPositionForNormal();
}
public void RefreshPositionForNormal()
{
var currentRect = SystemControl.GetCaptureRect(TaskContext.Instance().GameHandle);
Invoke(() =>
{
double dpiScale = DpiHelper.ScaleY;
Left = currentRect.Left / dpiScale;
Top = currentRect.Top / dpiScale;
Width = currentRect.Width / dpiScale;
Height = currentRect.Height / dpiScale;
BringToTop();
});
}
public MaskWindow()
{
_maskWindow = this;
this.SetResourceReference(StyleProperty, typeof(MaskWindow));
InitializeComponent();
this.InitializeDpiAwareness();
LogTextBox.TextChanged += LogTextBoxTextChanged;
//AddAreaSettingsControl("测试识别窗口");
Loaded += OnLoaded;
IsVisibleChanged += MaskWindowOnIsVisibleChanged;
StateChanged += MaskWindowOnStateChanged;
}
private void MaskWindowOnIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (IsVisible)
{
return;
}
if (DataContext is MaskWindowViewModel vm)
{
vm.PointInfoPopup.CloseCommand.Execute(null);
vm.IsMapPointPickerOpen = false;
}
if (_mapLabelSearchWindow != null)
{
_mapLabelSearchWindow.Hide();
}
}
private void MaskWindowOnStateChanged(object? sender, EventArgs e)
{
if (WindowState != WindowState.Minimized)
{
return;
}
if (DataContext is MaskWindowViewModel vm)
{
vm.PointInfoPopup.CloseCommand.Execute(null);
vm.IsMapPointPickerOpen = false;
}
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
_richTextBox = App.GetService();
if (_richTextBox != null)
{
_richTextBox.RichTextBox = LogTextBox;
}
_maskWindowConfig = TaskContext.Instance().Config.MaskWindowConfig;
_maskWindowConfig.PropertyChanged += MaskWindowConfigOnPropertyChanged;
_viewModel = DataContext as MaskWindowViewModel;
if (_viewModel != null)
{
_viewModel.PropertyChanged += ViewModelOnPropertyChanged;
}
UpdateClickThroughState();
RefreshPosition();
PrintSystemInfo();
PointsCanvasControl.ViewportChanged += PointsCanvasControlOnViewportChanged;
}
private void PointsCanvasControlOnViewportChanged(object? sender, EventArgs e)
{
if (_viewModel != null)
{
_viewModel.PointInfoPopup.CloseCommand.Execute(null);
_viewModel.IsMapPointPickerOpen = false;
}
}
protected override void OnClosed(EventArgs e)
{
PointsCanvasControl.ViewportChanged -= PointsCanvasControlOnViewportChanged;
IsVisibleChanged -= MaskWindowOnIsVisibleChanged;
StateChanged -= MaskWindowOnStateChanged;
if (_maskWindowConfig != null)
{
_maskWindowConfig.PropertyChanged -= MaskWindowConfigOnPropertyChanged;
}
if (_viewModel != null)
{
_viewModel.PropertyChanged -= ViewModelOnPropertyChanged;
}
if (_mapLabelSearchWindow != null)
{
_mapLabelSearchWindow.Close();
_mapLabelSearchWindow = null;
}
base.OnClosed(e);
}
private void MaskWindowConfigOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(MaskWindowConfig.OverlayLayoutEditEnabled))
{
Dispatcher.Invoke(UpdateClickThroughState);
}
}
private void ViewModelOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(MaskWindowViewModel.IsInBigMapUi) ||
e.PropertyName == nameof(MaskWindowViewModel.IsMapPointPickerOpen))
{
Dispatcher.Invoke(UpdateClickThroughState);
}
if (e.PropertyName == nameof(MaskWindowViewModel.IsMapPointPickerOpen))
{
if (_viewModel?.IsMapPointPickerOpen != true && _mapLabelSearchWindow != null)
{
Dispatcher.Invoke(() => _mapLabelSearchWindow.Hide());
}
}
}
private void UpdateClickThroughState()
{
try
{
var editEnabled = TaskContext.Instance().Config.MaskWindowConfig.OverlayLayoutEditEnabled;
var inBigMapUi = _viewModel?.IsInBigMapUi == true;
if (editEnabled)
{
this.SetClickThrough(false);
return;
}
this.SetClickThrough(!inBigMapUi);
}
catch
{
this.SetClickThrough(true);
}
}
private void PrintSystemInfo()
{
_logger.LogInformation("更好的原神 {Version}", Global.Version);
var systemInfo = TaskContext.Instance().SystemInfo;
var width = systemInfo.GameScreenSize.Width;
var height = systemInfo.GameScreenSize.Height;
var dpiScale = TaskContext.Instance().DpiScale;
_logger.LogInformation("遮罩窗口已启动,游戏大小{Width}x{Height},素材缩放{Scale},DPI缩放{Dpi}",
width, height, systemInfo.AssetScale.ToString("F"), dpiScale);
if (width * 9 != height * 16)
{
_logger.LogError("当前游戏分辨率不是16:9,一条龙、配队识别、地图传送、地图追踪等所有独立任务与全自动任务相关功能,都将会无法正常使用!");
}
AfterburnerWarning();
// 读取游戏注册表配置
GameSettingsChecker.LoadGameSettingsAndCheck();
}
/**
* MSIAfterburner.exe 在左上角会导致识别失败
*/
private void AfterburnerWarning()
{
if (Process.GetProcessesByName("MSIAfterburner").Length > 0)
{
_logger.LogWarning("检测到 MSI Afterburner 正在运行,如果信息位于特定UI上遮盖图像识别要素可能导致识别失败,请关闭MSI Afterburner 或者调整信息位置后重试!");
}
}
// private void ReadGameSettings()
// {
// try
// {
// SettingsContainer settings = new();
// TaskContext.Instance().GameSettings = settings;
// var lang = settings.Language?.TextLang;
// if (lang != null && lang != TextLanguage.SimplifiedChinese)
// {
// _logger.LogWarning("当前游戏语言{Lang}不是简体中文,部分功能可能无法正常使用。The game language is not Simplified Chinese, some functions may not work properly", lang);
// }
// }
// catch (Exception e)
// {
// _logger.LogWarning("游戏注册表配置信息读取失败:" + e.Source + "\r\n--" + Environment.NewLine + e.StackTrace + "\r\n---" + Environment.NewLine + e.Message);
// }
// }
protected override void OnSourceInitialized(EventArgs e)
{
base.OnSourceInitialized(e);
this.SetLayeredWindow();
this.SetChildWindow();
this.HideFromAltTab();
UpdateClickThroughState();
}
private void LogTextBoxTextChanged(object sender, TextChangedEventArgs e)
{
if (LogTextBox.Document.Blocks.FirstBlock is Paragraph p && p.Inlines.Count > 1000)
{
(p.Inlines as System.Collections.IList).RemoveAt(0);
}
var textRange = new TextRange(LogTextBox.Document.ContentStart, LogTextBox.Document.ContentEnd);
if (textRange.Text.Length > 10000)
{
LogTextBox.Document.Blocks.Clear();
}
LogTextBox.ScrollToEnd();
}
private void MapLabelSearchTextBox_OnPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (DataContext is not MaskWindowViewModel vm)
{
return;
}
if (_mapLabelSearchWindow == null)
{
_mapLabelSearchWindow = new MapLabelSearchWindow();
_mapLabelSearchWindow.AttachViewModel(vm);
}
var textbox = (FrameworkElement)sender;
var point = textbox.PointToScreen(new Point(0, 0));
var popupHeight = _mapLabelSearchWindow.ActualHeight > 0 ? _mapLabelSearchWindow.ActualHeight : _mapLabelSearchWindow.Height;
_mapLabelSearchWindow.Left = point.X / DpiHelper.ScaleY;
_mapLabelSearchWindow.Top = (point.Y - 4) / DpiHelper.ScaleY - popupHeight;
if (!_mapLabelSearchWindow.IsVisible)
{
_mapLabelSearchWindow.Show();
}
_mapLabelSearchWindow.Topmost = true;
_mapLabelSearchWindow.FocusSearch();
e.Handled = true;
}
private void MapLabelCategoriesListView_OnPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var container = ItemsControl.ContainerFromElement(MapLabelCategoriesListView, e.OriginalSource as DependencyObject) as ListViewItem;
if (container == null)
{
return;
}
var item = MapLabelCategoriesListView.ItemContainerGenerator.ItemFromContainer(container) as MapLabelCategoryVm;
if (item == null)
{
return;
}
if (ReferenceEquals(MapLabelCategoriesListView.SelectedItem, item))
{
return;
}
MapLabelCategoriesListView.SelectedItem = item;
if (DataContext is MaskWindowViewModel vm)
{
_mapLabelCategorySelectCts?.Cancel();
_mapLabelCategorySelectCts?.Dispose();
_mapLabelCategorySelectCts = new CancellationTokenSource();
_ = SelectMapLabelCategoryAsync(vm, item, _mapLabelCategorySelectCts.Token);
}
}
private async Task SelectMapLabelCategoryAsync(MaskWindowViewModel vm, MapLabelCategoryVm item, CancellationToken ct)
{
try
{
await vm.SelectMapLabelCategoryCommand.ExecuteAsync(item);
}
catch (Exception ex) when (!ct.IsCancellationRequested)
{
_logger.LogWarning(ex, "切换地图标点分类时发生异常");
}
}
public void Refresh()
{
Dispatcher.Invoke(InvalidateVisual);
}
public void Invoke(Action action)
{
try
{
Dispatcher.Invoke(action);
}
catch (TaskCanceledException)
{
}
catch (OperationCanceledException)
{
}
}
public void BeginInvoke(Action action)
{
try
{
Dispatcher.BeginInvoke(action);
}
catch (TaskCanceledException)
{
}
catch (OperationCanceledException)
{
}
}
public void HideSelf()
{
if (TaskContext.Instance().Config.MaskWindowConfig.OverlayLayoutEditEnabled)
{
return;
}
this.Hide();
}
protected override void OnRender(DrawingContext drawingContext)
{
try
{
var cnt = VisionContext.Instance().DrawContent.RectList.Count + VisionContext.Instance().DrawContent.LineList.Count + VisionContext.Instance().DrawContent.TextList.Count;
if (cnt == 0)
{
return;
}
var displayRecognitionResults = TaskContext.Instance().Config.MaskWindowConfig.DisplayRecognitionResultsOnMask;
if (!displayRecognitionResults)
{
return;
}
if (displayRecognitionResults)
{
foreach (var kv in VisionContext.Instance().DrawContent.RectList)
{
foreach (var drawable in kv.Value)
{
if (!drawable.IsEmpty)
{
drawingContext.DrawRectangle(
Brushes.Transparent,
new Pen(new SolidColorBrush(drawable.Pen.Color.ToWindowsColor()), drawable.Pen.Width),
drawable.Rect);
}
}
}
foreach (var kv in VisionContext.Instance().DrawContent.LineList)
{
foreach (var drawable in kv.Value)
{
drawingContext.DrawLine(new Pen(new SolidColorBrush(drawable.Pen.Color.ToWindowsColor()), drawable.Pen.Width), drawable.P1, drawable.P2);
}
}
foreach (var kv in VisionContext.Instance().DrawContent.TextList)
{
bool isSkillCd = kv.Key == "SkillCdText";
var systemInfo = TaskContext.Instance().SystemInfo;
var scaleTo1080 = systemInfo.ScaleTo1080PRatio;
foreach (var drawable in kv.Value)
{
if (!drawable.IsEmpty)
{
var pixelsPerDip = VisualTreeHelper.GetDpi(this).PixelsPerDip;
var renderPoint = new Point(drawable.Point.X / pixelsPerDip, drawable.Point.Y / pixelsPerDip);
if (isSkillCd)
{
var skillConfigScale = TaskContext.Instance().Config.SkillCdConfig.Scale;
double scaledFontSize = (26 * scaleTo1080 * skillConfigScale) / pixelsPerDip;
var mediumTypeface = new Typeface(_fgiTypeface.FontFamily, _fgiTypeface.Style, FontWeights.Medium, _fgiTypeface.Stretch);
bool isZeroCd =
double.TryParse(drawable.Text, NumberStyles.Float, CultureInfo.InvariantCulture, out var cdValue)
&& Math.Abs(cdValue) < 0.8;
var skillConfig = TaskContext.Instance().Config.SkillCdConfig;
string textColorStr = isZeroCd ? skillConfig.TextReadyColor : skillConfig.TextNormalColor;
string bgColorStr = isZeroCd ? skillConfig.BackgroundReadyColor : skillConfig.BackgroundNormalColor;
Color textColor = ParseColor(textColorStr) ?? (isZeroCd ? Color.FromRgb(93, 204, 23) : Color.FromRgb(218, 74, 35));
Color bgColor = ParseColor(bgColorStr) ?? Colors.White;
Brush textBrush = new SolidColorBrush(textColor);
Brush bgBrush = new SolidColorBrush(bgColor);
var formattedText = new FormattedText(
drawable.Text,
CultureInfo.GetCultureInfo("zh-cn"),
FlowDirection.LeftToRight,
mediumTypeface,
scaledFontSize,
textBrush,
pixelsPerDip);
double px = (6 * scaleTo1080 * skillConfigScale) / pixelsPerDip;
double py = (2 * scaleTo1080 * skillConfigScale) / pixelsPerDip;
double radius = (5 * scaleTo1080 * skillConfigScale) / pixelsPerDip;
var bgRect = new Rect(renderPoint.X - px, renderPoint.Y - py, formattedText.Width + px * 2, formattedText.Height + py * 2);
drawingContext.DrawRoundedRectangle(bgBrush, null, bgRect, radius, radius);
drawingContext.DrawText(formattedText, renderPoint);
}
else
{
double defaultFontSize = (36 * scaleTo1080) / pixelsPerDip;
drawingContext.DrawText(new FormattedText(drawable.Text,
CultureInfo.GetCultureInfo("zh-cn"),
FlowDirection.LeftToRight,
_typeface,
defaultFontSize, Brushes.Black, pixelsPerDip), renderPoint);
}
}
}
}
}
}
catch (Exception e)
{
Debug.WriteLine(e);
}
base.OnRender(drawingContext);
}
public RichTextBox LogBox => LogTextBox;
///
/// 解析颜色字符串(支持RGB和RGBA的16进制表示)
///
/// 颜色字符串,如 #RRGGBB 或 #RRGGBBAA
/// 解析后的Color,失败返回null
private static Color? ParseColor(string colorStr)
{
if (string.IsNullOrWhiteSpace(colorStr))
{
return null;
}
try
{
string hex = colorStr.Trim().TrimStart('#');
// 支持 #RRGGBB 或 #RRGGBBAA 格式
if (hex.Length == 6)
{
// RGB格式
byte r = byte.Parse(hex.Substring(0, 2), System.Globalization.NumberStyles.HexNumber);
byte g = byte.Parse(hex.Substring(2, 2), System.Globalization.NumberStyles.HexNumber);
byte b = byte.Parse(hex.Substring(4, 2), System.Globalization.NumberStyles.HexNumber);
return Color.FromArgb(255, r, g, b); // Alpha = 255 (完全不透明)
}
else if (hex.Length == 8)
{
// RGBA格式
byte r = byte.Parse(hex.Substring(0, 2), System.Globalization.NumberStyles.HexNumber);
byte g = byte.Parse(hex.Substring(2, 2), System.Globalization.NumberStyles.HexNumber);
byte b = byte.Parse(hex.Substring(4, 2), System.Globalization.NumberStyles.HexNumber);
byte a = byte.Parse(hex.Substring(6, 2), System.Globalization.NumberStyles.HexNumber);
return Color.FromArgb(a, r, g, b);
}
}
catch (Exception)
{
// 解析失败,返回null
}
return null;
}
}
file static class MaskWindowExtension
{
public static void HideFromAltTab(this Window window)
{
HideFromAltTab(new WindowInteropHelper(window).Handle);
}
public static void HideFromAltTab(nint hWnd)
{
int style = User32.GetWindowLong(hWnd, User32.WindowLongFlags.GWL_EXSTYLE);
style |= (int)User32.WindowStylesEx.WS_EX_TOOLWINDOW;
User32.SetWindowLong(hWnd, User32.WindowLongFlags.GWL_EXSTYLE, style);
}
public static void SetLayeredWindow(this Window window, bool isLayered = true)
{
SetLayeredWindow(new WindowInteropHelper(window).Handle, isLayered);
}
private static void SetLayeredWindow(nint hWnd, bool isLayered = true)
{
int style = User32.GetWindowLong(hWnd, User32.WindowLongFlags.GWL_EXSTYLE);
if (isLayered)
{
style |= (int)User32.WindowStylesEx.WS_EX_TRANSPARENT;
style |= (int)User32.WindowStylesEx.WS_EX_LAYERED;
}
else
{
style &= ~(int)User32.WindowStylesEx.WS_EX_TRANSPARENT;
style &= ~(int)User32.WindowStylesEx.WS_EX_LAYERED;
}
_ = User32.SetWindowLong(hWnd, User32.WindowLongFlags.GWL_EXSTYLE, style);
}
public static void SetClickThrough(this Window window, bool isClickThrough)
{
SetLayeredWindow(new WindowInteropHelper(window).Handle, isClickThrough);
}
public static void SetChildWindow(this Window window)
{
SetChildWindow(new WindowInteropHelper(window).Handle);
}
private static void SetChildWindow(nint hWnd)
{
int style = User32.GetWindowLong(hWnd, User32.WindowLongFlags.GWL_STYLE);
style |= (int)User32.WindowStyles.WS_CHILD;
_ = User32.SetWindowLong(hWnd, User32.WindowLongFlags.GWL_STYLE, style);
}
}