Files
Shigure/Runtime/PixelScanner.cs
waynebian01 5a00da85d5 Refactor and implement runtime session management
- Removed the UnitSelector class from UI and added a new UnitSelector class in Modules.
- Introduced RuntimeSessionCoordinator to manage runtime sessions, ensuring safe concurrent operations.
- Created ShigureRuntimeFactory to handle the creation of ShigureRuntime instances.
- Added WindowsTriggerKeyState and WindowsVirtualKeyMap for handling key states and virtual key mappings.
- Implemented IKeymapResolver interface for key mapping resolution.
- Established runtime dependencies with interfaces for screen scanning, state building, logic evaluation, and key output.
2026-08-06 01:17:28 +08:00

375 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
namespace Shigure;
public sealed record ScreenScanResult(
IReadOnlyDictionary<int, int>? RowData,
IReadOnlyDictionary<int, int> BarData,
IReadOnlyDictionary<int, int> HealAbsorbData,
string? FailureReason);
public sealed class PixelScanner : IRuntimeScreenScanner
{
private const int TopRowBlockCount = 510;
private const int TopRowFirstSchemeMax = 255;
private const int HealAbsorbMaxRows = 6;
private const int HealAbsorbMaxUnits = 30;
private readonly string _windowTitle;
public PixelScanner(string windowTitle)
{
_windowTitle = windowTitle;
try
{
NativeMethods.SetProcessDPIAware();
}
catch
{
// DPI awareness is best effort.
}
}
public ScreenScanResult ScanScreenData()
{
var emptyBars = new Dictionary<int, int>();
var emptyAbsorb = new Dictionary<int, int>();
var hwnd = NativeMethods.FindWindow(null, _windowTitle);
if (hwnd == 0)
{
return new ScreenScanResult(null, emptyBars, emptyAbsorb, $"未找到目标窗口“{_windowTitle}”");
}
if (NativeMethods.IsIconic(hwnd))
{
return new ScreenScanResult(null, emptyBars, emptyAbsorb, $"目标窗口“{_windowTitle}”已最小化");
}
var point = new NativeMethods.Point(0, 0);
if (!NativeMethods.ClientToScreen(hwnd, ref point))
{
return new ScreenScanResult(
null,
emptyBars,
emptyAbsorb,
$"无法获取目标窗口的屏幕坐标Win32 错误码: {Marshal.GetLastWin32Error()}");
}
if (!NativeMethods.GetClientRect(hwnd, out var rect))
{
return new ScreenScanResult(
null,
emptyBars,
emptyAbsorb,
$"无法获取目标窗口的客户区尺寸Win32 错误码: {Marshal.GetLastWin32Error()}");
}
var width = rect.Right - rect.Left;
var height = rect.Bottom - rect.Top;
if (width <= 0 || height <= 0)
{
return new ScreenScanResult(null, emptyBars, emptyAbsorb, $"目标窗口客户区尺寸无效: {width}×{height}");
}
try
{
var rowData = ScanTopRow(point.X, point.Y, width);
var markerY = FindCountBarsMarkerY(point.X, point.Y, height);
var barData = markerY is null
? emptyBars
: ScanLeftMarkerRow(point.X, point.Y + markerY.Value, width);
var healAbsorbData = markerY is null
? emptyAbsorb
: ScanHealAbsorbGrid(point.X, point.Y, width, height, markerY.Value);
return rowData.Count == 0
? new ScreenScanResult(null, barData, healAbsorbData, "未找到有效的状态像素起始标记")
: new ScreenScanResult(
rowData,
barData,
healAbsorbData,
markerY is null ? "未找到 CountBars 标记,层数条和治疗吸收数据未采集" : null);
}
catch (Exception ex)
{
return new ScreenScanResult(null, emptyBars, emptyAbsorb, $"{ex.GetType().Name}: {ex.Message}");
}
}
private static Dictionary<int, int> ScanTopRow(int baseX, int baseY, int width)
{
var rowData = new Dictionary<int, int>();
using var top = Capture(baseX, baseY, width, 1);
var pixels = ReadPixels(top);
var startX = -1;
for (var x = 0; x < Math.Min(TopRowBlockCount, width); x++)
{
var color = Color.FromArgb(pixels[x]);
if (TryDecodeTopRowBlock(color, out var step, out _) && step == 1)
{
startX = x;
break;
}
}
if (startX < 0)
{
return rowData;
}
for (var x = startX; x < width; x++)
{
var color = Color.FromArgb(pixels[x]);
if (TryDecodeTopRowBlock(color, out var step, out var value))
{
rowData[step] = value;
if (step == TopRowBlockCount)
{
break;
}
}
}
return rowData;
}
private static int? FindCountBarsMarkerY(int baseX, int baseY, int height)
{
using var left = Capture(baseX, baseY, 1, height);
var leftPixels = ReadPixels(left);
for (var y = 0; y < height; y++)
{
if (IsRedMarker(Color.FromArgb(leftPixels[y])))
{
return y;
}
}
return null;
}
private static Dictionary<int, int> ScanLeftMarkerRow(int baseX, int rowScreenY, int width)
{
var barData = new Dictionary<int, int>();
using var markerRow = Capture(baseX, rowScreenY, width, 1);
var rowPixels = ReadPixels(markerRow);
var segIndex = 0;
var x = 0;
var pendingRed = false;
while (x < width)
{
var color = Color.FromArgb(rowPixels[x]);
if (IsGrayEndMarker(color))
{
break;
}
if (pendingRed && IsRedGreenMarker(color))
{
pendingRed = false;
segIndex++;
var (value, nextX) = ConsumeValueFrom(rowPixels, x + 1, alreadySawWhite: false);
barData[segIndex] = Math.Max(0, value - 1);
x = nextX;
continue;
}
if (IsRedMarker(color))
{
pendingRed = true;
x++;
continue;
}
if (IsWhite(color))
{
var prevWhite = x > 0 && IsWhite(Color.FromArgb(rowPixels[x - 1]));
if (!prevWhite)
{
pendingRed = false;
segIndex++;
var (value, nextX) = ConsumeValueFrom(rowPixels, x + 1, alreadySawWhite: true);
barData[segIndex] = Math.Max(0, value - 1);
x = nextX;
continue;
}
}
x++;
}
return barData;
}
/// <summary>
/// 扫描 CountBars 下方的治疗吸收网格。
/// 与层数条相同读纯白块右侧第一个非白像素G-1 为吸收值B 为单位编号1..30)。
/// </summary>
private static Dictionary<int, int> ScanHealAbsorbGrid(int baseX, int baseY, int width, int height, int countBarsY)
{
var result = new Dictionary<int, int>();
for (var row = 0; row < HealAbsorbMaxRows; row++)
{
var rowY = countBarsY + 1 + row;
if (rowY >= height)
{
break;
}
using var rowBmp = Capture(baseX, baseY + rowY, width, 1);
var rowPixels = ReadPixels(rowBmp);
var x = 0;
while (x < width)
{
var color = Color.FromArgb(rowPixels[x]);
if (!IsWhite(color))
{
x++;
continue;
}
var prevWhite = x > 0 && IsWhite(Color.FromArgb(rowPixels[x - 1]));
if (prevWhite)
{
x++;
continue;
}
var (green, blue, nextX) = ConsumeHealAbsorbPixel(rowPixels, x + 1);
if (blue is >= 1 and <= HealAbsorbMaxUnits)
{
result[blue] = Math.Max(0, green - 1);
}
x = nextX;
}
}
return result;
}
private static (int Value, int NextX) ConsumeValueFrom(int[] row, int fromX, bool alreadySawWhite)
{
var sx = fromX;
var needWhite = !alreadySawWhite;
while (sx < row.Length)
{
var color = Color.FromArgb(row[sx]);
if (IsGrayEndMarker(color))
{
return (0, row.Length);
}
if (IsRedMarker(color))
{
return (0, sx);
}
if (needWhite)
{
if (IsWhite(color))
{
needWhite = false;
}
sx++;
continue;
}
if (IsWhite(color))
{
sx++;
continue;
}
return (color.G, sx + 1);
}
return (0, row.Length);
}
/// <summary>
/// 白条已开始:跳过后续白像素,读右侧第一个非白像素的 G吸收索引与 B单位编号
/// </summary>
private static (int Green, int Blue, int NextX) ConsumeHealAbsorbPixel(int[] row, int fromX)
{
var sx = fromX;
while (sx < row.Length)
{
var color = Color.FromArgb(row[sx]);
if (IsWhite(color))
{
sx++;
continue;
}
return (color.G, color.B, sx + 1);
}
return (0, 0, row.Length);
}
private static Bitmap Capture(int x, int y, int width, int height)
{
var bitmap = new Bitmap(width, height, PixelFormat.Format32bppArgb);
using var graphics = Graphics.FromImage(bitmap);
graphics.CopyFromScreen(x, y, 0, 0, new Size(width, height), CopyPixelOperation.SourceCopy);
return bitmap;
}
// 32bpp 位图 stride 恒为 width*4(无填充), 一次 LockBits + Marshal.Copy 读完整张为 0xAARRGGBB,
// 取代逐像素 GetPixel(每次都会 Lock/UnlockBits)。Color.FromArgb 还原后 R/G/B 与原先一致。
private static int[] ReadPixels(Bitmap bitmap)
{
var data = bitmap.LockBits(
new Rectangle(0, 0, bitmap.Width, bitmap.Height),
ImageLockMode.ReadOnly,
PixelFormat.Format32bppArgb);
try
{
var pixels = new int[bitmap.Width * bitmap.Height];
Marshal.Copy(data.Scan0, pixels, 0, pixels.Length);
return pixels;
}
finally
{
bitmap.UnlockBits(data);
}
}
private static bool IsRedMarker(Color color) => color.R == 1 && color.G == 0 && color.B == 0;
private static bool IsRedGreenMarker(Color color) => color.R == 1 && color.G == 1 && color.B == 0;
private static bool IsWhite(Color color) => color.R == 255 && color.G == 255 && color.B == 255;
private static bool IsGrayEndMarker(Color color) => color.R == 200 && color.G == 200 && color.B == 200;
private static bool TryDecodeTopRowBlock(Color color, out int step, out int value)
{
step = 0;
value = 0;
if (color.G is < 1 or > TopRowFirstSchemeMax)
{
return false;
}
step = color.R switch
{
0 => color.G,
1 => TopRowFirstSchemeMax + color.G,
_ => 0
};
if (step is < 1 or > TopRowBlockCount)
{
step = 0;
return false;
}
value = color.B;
return true;
}
}