提供老版本的稳定 BitBlt 功能

This commit is contained in:
辉鸭蛋
2025-04-11 00:11:42 +08:00
parent 6b306582d0
commit 1df15bb83c
21 changed files with 319 additions and 48 deletions

View File

@@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
using System.ComponentModel;
namespace BetterGenshinImpact.GameTask.AutoFishing
{

View File

@@ -1,6 +1,7 @@
using BetterGenshinImpact.GameTask.Model.Area;
using System;
using System.Drawing;
using Fischless.GameCapture;
using OpenCvSharp;
namespace BetterGenshinImpact.GameTask;
@@ -19,7 +20,7 @@ public class CaptureContent : IDisposable
public ImageRegion CaptureRectArea { get; private set; }
public CaptureContent(Mat image, int frameIndex, double interval)
public CaptureContent(CaptureImageRes image, int frameIndex, double interval)
{
FrameIndex = frameIndex;
TimerInterval = interval;

View File

@@ -17,13 +17,13 @@ public class TaskControl
public static ILogger Logger { get; } = App.GetLogger<TaskControl>();
public static readonly SemaphoreSlim TaskSemaphore = new(1, 1);
public static void CheckAndSleep(int millisecondsTimeout)
{
TrySuspend();
CheckAndActivateGameWindow();
Thread.Sleep(millisecondsTimeout);
}
@@ -33,10 +33,10 @@ public class TaskControl
{
TrySuspend();
CheckAndActivateGameWindow();
}, TimeSpan.FromSeconds(1), 100);
Thread.Sleep(millisecondsTimeout);
}
private static bool IsKeyPressed(User32.VK key)
{
// 获取按键状态
@@ -45,6 +45,7 @@ public class TaskControl
// 检查高位是否为 1表示按键被按下
return (state & 0x8000) != 0;
}
public static void TrySuspend()
{
var first = true;
@@ -65,6 +66,7 @@ public class TaskControl
Simulation.SendInput.Keyboard.KeyUp(key);
}
}
Logger.LogWarning("快捷键触发暂停,等待解除");
foreach (var item in RunnerContext.Instance.SuspendableDictionary)
{
@@ -76,6 +78,7 @@ public class TaskControl
Thread.Sleep(1000);
}
//从暂停中解除
if (isSuspend)
{
@@ -97,12 +100,12 @@ public class TaskControl
throw new RetryException("当前获取焦点的窗口不是原神");
}
}
var count = 0;
//未激活则尝试恢复窗口
while (!SystemControl.IsGenshinImpactActiveByProcess())
{
if (count>=10 && count%10==0)
if (count >= 10 && count % 10 == 0)
{
Logger.LogInformation("多次尝试未恢复,尝试最小化后激活窗口!");
SystemControl.MinimizeAndActivateWindow(TaskContext.Instance().GameHandle);
@@ -112,6 +115,7 @@ public class TaskControl
Logger.LogInformation("当前获取焦点的窗口不是原神,尝试恢复窗口");
SystemControl.FocusWindow(TaskContext.Instance().GameHandle);
}
count++;
Thread.Sleep(1000);
}
@@ -135,10 +139,9 @@ public class TaskControl
{
throw new NormalEndException("取消自动任务");
}
TrySuspend();
CheckAndActivateGameWindow();
}, TimeSpan.FromSeconds(1), 100);
Thread.Sleep(millisecondsTimeout);
if (ct.IsCancellationRequested)
@@ -165,9 +168,9 @@ public class TaskControl
{
throw new NormalEndException("取消自动任务");
}
TrySuspend();
CheckAndActivateGameWindow();
}, TimeSpan.FromSeconds(1), 100);
await Task.Delay(millisecondsTimeout, ct);
if (ct is { IsCancellationRequested: true })
@@ -176,7 +179,7 @@ public class TaskControl
}
}
public static Mat CaptureGameImage(IGameCapture? gameCapture)
public static CaptureImageRes CaptureGameImage(IGameCapture? gameCapture)
{
var image = gameCapture?.Capture();
if (image == null)
@@ -190,10 +193,10 @@ public class TaskControl
{
return image;
}
Sleep(30);
}
throw new Exception("尝试多次后,截图失败!");
}
else
@@ -201,8 +204,8 @@ public class TaskControl
return image;
}
}
public static Mat? CaptureGameImageNoRetry(IGameCapture? gameCapture)
public static CaptureImageRes? CaptureGameImageNoRetry(IGameCapture? gameCapture)
{
return gameCapture?.Capture();
}
@@ -213,8 +216,8 @@ public class TaskControl
/// <returns></returns>
public static ImageRegion CaptureToRectArea(bool forceNew = false)
{
var image =CaptureGameImage(TaskTriggerDispatcher.GlobalGameCapture);
var image = CaptureGameImage(TaskTriggerDispatcher.GlobalGameCapture);
var content = new CaptureContent(image, 0, 0);
return content.CaptureRectArea;
}
}
}

View File

@@ -3,6 +3,7 @@ using BetterGenshinImpact.GameTask.Model.Area.Converter;
using BetterGenshinImpact.Helpers;
using Fischless.WindowsInput;
using System.Drawing;
using Fischless.GameCapture;
using OpenCvSharp;
namespace BetterGenshinImpact.GameTask.Model.Area;
@@ -68,7 +69,7 @@ public class DesktopRegion : Region
Simulation.SendInput.Mouse.MoveMouseBy((int)dx, (int)dy);
}
public GameCaptureRegion Derive(Mat captureMat, int x, int y)
public GameCaptureRegion Derive(CaptureImageRes captureMat, int x, int y)
{
return new GameCaptureRegion(captureMat, x, y, this, new TranslationConverter(x, y));
}

View File

@@ -3,6 +3,7 @@ using BetterGenshinImpact.View.Drawable;
using OpenCvSharp;
using System;
using System.Drawing;
using Fischless.GameCapture;
using Size = OpenCvSharp.Size;
namespace BetterGenshinImpact.GameTask.Model.Area;
@@ -11,8 +12,19 @@ namespace BetterGenshinImpact.GameTask.Model.Area;
/// 游戏捕获区域类
/// 主要用于转换到遮罩窗口的坐标
/// </summary>
public class GameCaptureRegion(Mat mat, int initX, int initY, Region? owner = null, INodeConverter? converter = null, DrawContent? drawContent = null) : ImageRegion(mat, initX, initY, owner, converter, drawContent)
public class GameCaptureRegion : ImageRegion
{
public GameCaptureRegion(Mat mat, int initX, int initY, Region? owner = null, INodeConverter? converter = null,
DrawContent? drawContent = null) : base(mat, initX, initY, owner, converter, drawContent)
{
}
public GameCaptureRegion(CaptureImageRes image, int initX, int initY, Region? owner = null,
INodeConverter? converter = null, DrawContent? drawContent = null) : base(image, initX, initY, owner, converter, drawContent)
{
}
/// <summary>
/// 在游戏捕获图像的坐标维度进行转换到遮罩窗口的坐标维度
/// </summary>
@@ -48,6 +60,7 @@ public class GameCaptureRegion(Mat mat, int initX, int initY, Region? owner = nu
{
drawable.Pen = pen;
}
return drawable;
}
@@ -66,6 +79,7 @@ public class GameCaptureRegion(Mat mat, int initX, int initY, Region? owner = nu
{
return this;
}
var scale = Width / 1920d;
var newMat = new Mat();
@@ -109,7 +123,7 @@ public class GameCaptureRegion(Mat mat, int initX, int initY, Region? owner = nu
var (dx, dy) = deltaFunc(new Size(captureAreaRect.Width, captureAreaRect.Height), assetScale);
DesktopRegion.DesktopRegionMoveBy(dx, dy);
}
/// <summary>
/// 静态方法,输入1080P下的坐标,方法会自动转换到当前游戏捕获区域大小下的坐标并点击
/// </summary>
@@ -126,4 +140,4 @@ public class GameCaptureRegion(Mat mat, int initX, int initY, Region? owner = nu
// 1080P坐标 转换到实际游戏窗口坐标
GameRegionMove((_, scale) => (cx * scale, cy * scale));
}
}
}

View File

@@ -13,6 +13,7 @@ using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text.RegularExpressions;
using Fischless.GameCapture;
using Point = OpenCvSharp.Point;
namespace BetterGenshinImpact.GameTask.Model.Area;
@@ -77,6 +78,23 @@ public class ImageRegion : Region
_srcMat = mat;
}
public ImageRegion(CaptureImageRes image, int x, int y, Region? owner = null, INodeConverter? converter = null,
DrawContent? drawContent = null) : base(x, y, image.Width, image.Height, owner, converter, drawContent)
{
if (image.Bitmap != null)
{
_srcBitmap = image.Bitmap;
}
else if (image.Mat != null)
{
_srcMat = image.Mat;
}
else
{
throw new Exception("ImageRegion的构造函数参数错误");
}
}
private bool HasImage()
{
return _srcBitmap != null || _srcMat != null;

View File

@@ -386,19 +386,24 @@ namespace BetterGenshinImpact.GameTask
Directory.CreateDirectory(path);
}
var bitmap = TaskControl.CaptureGameImage(GameCapture);
var image = TaskControl.CaptureGameImage(GameCapture);
var mat = image.ForceGetMat();
if (mat == null)
{
_logger.LogInformation("截图失败,未获取到图像");
return;
}
var name = $@"{DateTime.Now:yyyyMMddHHmmssffff}.png";
var savePath = Global.Absolute($@"log\screenshot\{name}");
if (TaskContext.Instance().Config.CommonConfig.ScreenshotUidCoverEnabled)
{
var rect = TaskContext.Instance().Config.MaskWindowConfig.UidCoverRect;
bitmap.Rectangle(rect, Scalar.White, -1);
Cv2.ImWrite(savePath, bitmap);
mat.Rectangle(rect, Scalar.White, -1);
Cv2.ImWrite(savePath, mat);
}
else
{
Cv2.ImWrite(savePath, bitmap);
Cv2.ImWrite(savePath, mat);
}
_logger.LogInformation("截图已保存: {Name}", name);

View File

@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using BetterGenshinImpact.Model;
namespace BetterGenshinImpact.Helpers.Extensions;
public static class EnumExtensions
{
public static string GetDescription(this Enum value)
{
return value.GetType()
.GetField(value.ToString())
?.GetCustomAttributes(typeof(DescriptionAttribute), false)
.Cast<DescriptionAttribute>()
.FirstOrDefault()
?.Description ?? value.ToString();
}
public static IEnumerable<EnumItem<T>> ToEnumItems<T>() where T : Enum
{
return Enum.GetValues(typeof(T))
.Cast<T>()
.Select(EnumItem<T>.Create);
}
}

View File

@@ -0,0 +1,21 @@
using System;
using BetterGenshinImpact.Helpers.Extensions;
namespace BetterGenshinImpact.Model;
public class EnumItem<T> where T : Enum
{
public T Value { get; set; }
public string DisplayName { get; set; }
public string EnumName => Value.ToString();
// 提供一个静态工厂方法来创建实例
public static EnumItem<T> Create(T value)
{
return new EnumItem<T>
{
Value = value,
DisplayName = value.GetDescription() // 使用扩展方法获取Description
};
}
}

View File

@@ -406,8 +406,19 @@ public class NotificationService : IHostedService, IDisposable
try
{
var bitmap = TaskControl.CaptureGameImageNoRetry(TaskTriggerDispatcher.GlobalGameCapture);
if (bitmap != null) notificationData.Screenshot = bitmap.ToBitmap();
var image = TaskControl.CaptureGameImageNoRetry(TaskTriggerDispatcher.GlobalGameCapture);
if (image != null)
{
if (image.Bitmap != null)
{
notificationData.Screenshot = image.Bitmap;
}
else if (image.Mat != null)
{
notificationData.Screenshot = image.Mat.ToBitmap();
}
}
}
catch (Exception ex)
{

View File

@@ -61,11 +61,12 @@ public partial class CaptureTestWindow : Window
{
var sw = new Stopwatch();
sw.Start();
var bitmap = _capture?.Capture();
var image = _capture?.Capture();
sw.Stop();
Debug.WriteLine("截图耗时:" + sw.ElapsedMilliseconds);
_captureTime += sw.ElapsedMilliseconds;
var bitmap = image?.ForceGetBitmap();
if (bitmap != null)
{
Debug.WriteLine($"Bitmap:{bitmap.Width}x{bitmap.Height}");

View File

@@ -134,14 +134,16 @@
<ui:TextBlock Grid.Row="1"
Grid.Column="0"
Foreground="{ui:ThemeResource TextFillColorTertiaryBrush}"
Text="如果可用的话,推荐选择 BitBlt问题较少"
Text="推荐选择 BitBlt(稳定),问题较少"
TextWrapping="Wrap" />
<ComboBox Grid.Row="0"
Grid.RowSpan="2"
Grid.Column="1"
Margin="0,0,36,0"
ItemsSource="{Binding ModeNames, Mode=OneWay}"
SelectedItem="{Binding Config.CaptureMode, Mode=TwoWay}">
DisplayMemberPath="DisplayName"
SelectedValuePath="EnumName"
SelectedValue="{Binding Config.CaptureMode, Mode=TwoWay}">
<b:Interaction.Triggers>
<b:EventTrigger EventName="SelectionChanged">
<b:InvokeCommandAction Command="{Binding CaptureModeDropDownChangedCommand}" CommandParameter="GeniusInvocation" />

View File

@@ -15,22 +15,30 @@ using CommunityToolkit.Mvvm.Messaging.Messages;
using Fischless.GameCapture;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Interop;
using Windows.System;
using BetterGenshinImpact.GameTask.AutoFishing;
using BetterGenshinImpact.Helpers.Extensions;
using BetterGenshinImpact.Model;
using Wpf.Ui.Controls;
namespace BetterGenshinImpact.ViewModel.Pages;
public partial class HomePageViewModel : ViewModel
{
[ObservableProperty]
private string[] _modeNames = GameCaptureFactory.ModeNames();
private IEnumerable<EnumItem<CaptureModes>> _modeNames = EnumExtensions.ToEnumItems<CaptureModes>();
[ObservableProperty]
private string? _selectedMode = CaptureModes.BitBlt.ToString();
@@ -66,11 +74,14 @@ public partial class HomePageViewModel : ViewModel
Config = configService.Get();
ReadGameInstallPath();
// WindowsGraphicsCapture 只支持 Win10 18362 及以上的版本 (Windows 10 version 1903 or later)
// https://github.com/babalae/better-genshin-impact/issues/394
if (!OsVersionHelper.IsWindows10_1903_OrGreater)
{
_modeNames = _modeNames.Where(x => x != CaptureModes.WindowsGraphicsCapture.ToString()).ToArray();
// 删除 _modeNames 中的 CaptureModes.WindowsGraphicsCapture
_modeNames = _modeNames.Where(x => x.EnumName != CaptureModes.WindowsGraphicsCapture.ToString()).ToList();
// DirectML 是在 Windows 10 版本 1903 和 Windows SDK 的相应版本中引入的。
// https://learn.microsoft.com/zh-cn/windows/ai/directml/dml
_inferenceDeviceTypes = _inferenceDeviceTypes.Where(x => x != "GPU_DirectML").ToArray();

View File

@@ -18,7 +18,7 @@ public class BitBltCapture : IGameCapture
public void Dispose() => Stop();
public Mat? Capture() => Capture(false);
public CaptureImageRes? Capture() => Capture(false);
public void Start(nint hWnd, Dictionary<string, object>? settings = null)
{
@@ -111,7 +111,7 @@ public class BitBltCapture : IGameCapture
/// </summary>
/// <param name="recursive">递归标志</param>
/// <returns>截图</returns>
private Mat? Capture(bool recursive)
private CaptureImageRes? Capture(bool recursive)
{
if (_hWnd == IntPtr.Zero)
{
@@ -140,13 +140,13 @@ public class BitBltCapture : IGameCapture
{
// 成功截图
_lastCaptureFailed = false;
return result;
return CaptureImageRes.BuildNullable(result);
}
else if (result is null)
{
if (_lastCaptureFailed) return result; // 这不是首次失败,不再进行尝试
if (_lastCaptureFailed) return CaptureImageRes.BuildNullable(result); // 这不是首次失败,不再进行尝试
_lastCaptureFailed = true; // 设置失败标志
if (recursive) return result; // 已设置递归标志,说明也不是首次失败
if (recursive) return CaptureImageRes.BuildNullable(result); // 已设置递归标志,说明也不是首次失败
}
}
finally

View File

@@ -0,0 +1,72 @@
using System.Diagnostics;
using Vanara.PInvoke;
namespace Fischless.GameCapture.BitBlt;
public class BitBltOldCapture : IGameCapture
{
private nint _hWnd;
public CaptureModes Mode => CaptureModes.BitBltOld;
public static object LockObject { get; } = new();
public bool IsCapturing { get; private set; }
public void Dispose() => Stop();
public void Start(nint hWnd, Dictionary<string, object>? settings = null)
{
_hWnd = hWnd;
IsCapturing = true;
if (settings != null && settings.TryGetValue("autoFixWin11BitBlt", out var value))
{
if (value is true)
{
BitBltRegistryHelper.SetDirectXUserGlobalSettings();
}
}
}
public CaptureImageRes? Capture()
{
if (_hWnd == IntPtr.Zero)
{
return null;
}
try
{
lock (LockObject)
{
User32.GetClientRect(_hWnd, out var windowRect);
int x = default, y = default;
var width = windowRect.right - windowRect.left;
var height = windowRect.bottom - windowRect.top;
Bitmap bitmap = new(width, height);
using System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(bitmap);
var hdcDest = g.GetHdc();
var hdcSrc = User32.GetDC(_hWnd == IntPtr.Zero ? User32.GetDesktopWindow() : _hWnd);
Gdi32.StretchBlt(hdcDest, 0, 0, width, height, hdcSrc, x, y, width, height,
Gdi32.RasterOperationMode.SRCCOPY);
g.ReleaseHdc();
Gdi32.DeleteDC(hdcDest);
Gdi32.DeleteDC(hdcSrc);
return CaptureImageRes.BuildNullable(bitmap);
}
}
catch (Exception e)
{
Debug.WriteLine(e);
}
return null!;
}
public void Stop()
{
_hWnd = IntPtr.Zero;
IsCapturing = false;
}
}

View File

@@ -0,0 +1,76 @@
using OpenCvSharp;
using OpenCvSharp.Extensions;
namespace Fischless.GameCapture;
/// <summary>
/// 捕获的图像
/// </summary>
public class CaptureImageRes : IDisposable
{
public Bitmap? Bitmap { get; set; }
public Mat? Mat { get; set; }
public int Width => Mat?.Width ?? Bitmap?.Width ?? 0;
public int Height => Mat?.Height ?? Bitmap?.Height ?? 0;
public CaptureImageRes(Mat mat)
{
Mat = mat;
}
public CaptureImageRes(Bitmap bitmap)
{
Bitmap = bitmap;
}
public static CaptureImageRes? BuildNullable(Bitmap? bitmap)
{
if (bitmap == null)
{
return null;
}
return new CaptureImageRes(bitmap);
}
public static CaptureImageRes? BuildNullable(Mat? mat)
{
if (mat == null)
{
return null;
}
return new CaptureImageRes(mat);
}
/// <summary>
/// 非特殊情况不要使用这个方法,会造成额外的性能消耗
/// </summary>
/// <returns></returns>
public Mat? ForceGetMat()
{
if (Mat == null)
{
Mat = Bitmap?.ToMat();
}
return Mat;
}
/// <summary>
/// 非特殊情况不要使用这个方法,会造成额外的性能消耗
/// </summary>
/// <returns></returns>
public Bitmap? ForceGetBitmap()
{
if (Bitmap == null)
{
Bitmap = Mat?.ToBitmap();
}
return Bitmap;
}
public void Dispose()
{
Bitmap?.Dispose();
Mat?.Dispose();
}
}

View File

@@ -1,8 +1,18 @@
namespace Fischless.GameCapture;
using System.ComponentModel;
namespace Fischless.GameCapture;
public enum CaptureModes
{
[Description("BitBlt(稳定)")]
BitBltOld,
[Description("BitBlt(极速)")]
BitBlt,
[Description("WindowsGraphicsCapture")]
WindowsGraphicsCapture,
[Description("DwmGetDxSharedSurface")]
DwmGetDxSharedSurface
}

View File

@@ -67,7 +67,7 @@ namespace Fischless.GameCapture.DwmSharedSurface
return region;
}
public Mat? Capture()
public CaptureImageRes? Capture()
{
if (_hWnd == nint.Zero)
{
@@ -98,7 +98,7 @@ namespace Fischless.GameCapture.DwmSharedSurface
}
var bgrMat = new Mat();
Cv2.CvtColor(mat, bgrMat, ColorConversionCodes.BGRA2BGR);
return bgrMat;
return CaptureImageRes.BuildNullable(bgrMat);
}
}

View File

@@ -12,6 +12,7 @@ public class GameCaptureFactory
return mode switch
{
CaptureModes.BitBlt => new BitBlt.BitBltCapture(),
CaptureModes.BitBltOld => new BitBlt.BitBltOldCapture(),
CaptureModes.WindowsGraphicsCapture => new Graphics.GraphicsCapture(),
CaptureModes.DwmGetDxSharedSurface => new DwmSharedSurface.SharedSurfaceCapture(),
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null),

View File

@@ -278,7 +278,7 @@ public class GraphicsCapture : IGameCapture
return sdkMat;
}
public Mat? Capture()
public CaptureImageRes? Capture()
{
// 使用读锁获取最新帧
_frameAccessLock.EnterReadLock();
@@ -291,7 +291,7 @@ public class GraphicsCapture : IGameCapture
}
// 返回最新帧的副本这里我们必须克隆因为Mat是不线程安全的
return _latestFrame.Clone();
return CaptureImageRes.BuildNullable(_latestFrame.Clone());
}
finally
{

View File

@@ -9,7 +9,7 @@ public interface IGameCapture : IDisposable
public void Start(nint hWnd, Dictionary<string, object>? settings = null);
public Mat? Capture();
public CaptureImageRes? Capture();
public void Stop();
}