大學宿舍玩游戲的時候,為了簡化重復的鍵鼠動作,有學習過按鍵精靈和TC腳本開發工具,並做了一些小腳本,基本達到了當時的需求。不知不覺,已經畢業了3年了,無聊之余又玩起了游戲,對於一些無趣的重復行為,於是又想寫個腳本來處理下。比如跑任務,自動補血等,沒想到現在的游戲對於按鍵精靈和TC基本上都是封殺。對於我這種小白,過游戲安全檢測這種棘手的事,也許花費很多時間,都沒有結果。經常測試,發現游戲不會對自己寫的C#腳本進行檢測,所以決定用C#來寫。
研究了幾天,突然間又不想玩游戲了,所以把這幾天的研究成果分享給大家,希望對后來的人有啟發。我玩的是一款QQ的游戲,我想要做的腳本就是 掃貨腳本(當有人擺攤價格低於自己預設的價格時,自動購買下來,倒賣)。
經過分析,最難的步驟是怎么識別攤位上的價格,第一感覺,這不就是文字識別嗎,於是找了一個.Net 唯一開源的Tesseract-ocr。經過測試,發現Tesseract-ocr只適合白底黑字的文字識別,於是對圖片進行了以下處理
- 變灰度圖
- 增加亮度100
- 增加對比度100
- 變黑白
- //反向 游戲文字是白色的
/// <summary>
/// 反像
/// </summary>
/// <param name="bitmapImage"></param>
/// <returns></returns>
public static Bitmap ApplyInvert(Bitmap source)
{
//create a blank bitmap the same size as original
Bitmap newBitmap = new Bitmap(source.Width, source.Height);
//get a graphics object from the new image
Graphics g = Graphics.FromImage(newBitmap);
// create the negative color matrix
ColorMatrix colorMatrix = new ColorMatrix(new float[][]
{
new float[] {-1, 0, 0, 0, 0},
new float[] {0, -1, 0, 0, 0},
new float[] {0, 0, -1, 0, 0},
new float[] {0, 0, 0, 1, 0},
new float[] {1, 1, 1, 0, 1}
});
// create some image attributes
ImageAttributes attributes = new ImageAttributes();
attributes.SetColorMatrix(colorMatrix);
g.DrawImage(source, new Rectangle(0, 0, source.Width, source.Height),
0, 0, source.Width, source.Height, GraphicsUnit.Pixel, attributes);
//dispose the Graphics object
g.Dispose();
return newBitmap;
}
/// <summary>
/// 圖片變成灰度
/// </summary>
/// <param name="b"></param>
/// <returns></returns>
public static Bitmap ToGray(Bitmap b)
{
for (int x = 0; x < b.Width; x++)
{
for (int y = 0; y < b.Height; y++)
{
Color c = b.GetPixel(x, y);
int luma = (int)(c.R * 0.3 + c.G * 0.59 + c.B * 0.11);//轉換灰度的算法
b.SetPixel(x, y, Color.FromArgb(luma, luma, luma));
}
}
return b;
}
/// <summary>
/// 圖像變成黑白
/// </summary>
/// <param name="b"></param>
/// <returns></returns>
public static Bitmap ToBlackWhite(Bitmap b)
{
for (int x = 0; x < b.Width; x++)
{
for (int y = 0; y < b.Height; y++)
{
Color c = b.GetPixel(x, y);
if (c.R < (byte)255)
{
b.SetPixel(x, y, Color.FromArgb(0, 0, 0));
}
}
}
return b;
}
/// <summary>
/// 圖像亮度調整
/// </summary>
/// <param name="b"></param>
/// <param name="degree"></param>
/// <returns></returns>
public static Bitmap KiLighten(Bitmap b, int degree)
{
if (b == null)
{
return null;
}
if (degree < -255) degree = -255;
if (degree > 255) degree = 255;
try
{
int width = b.Width;
int height = b.Height;
int pix = 0;
BitmapData data = b.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
unsafe
{
byte* p = (byte*)data.Scan0;
int offset = data.Stride - width * 3;
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
// 處理指定位置像素的亮度
for (int i = 0; i < 3; i++)
{
pix = p[i] + degree;
if (degree < 0) p[i] = (byte)Math.Max(0, pix);
if (degree > 0) p[i] = (byte)Math.Min(255, pix);
} // i
p += 3;
} // x
p += offset;
} // y
}
b.UnlockBits(data);
return b;
}
catch
{
return null;
}
}
/// <summary>
/// 圖像對比度調整
/// </summary>
/// <param name="b">原始圖</param>
/// <param name="degree">對比度[-100, 100]</param>
/// <returns></returns>
public static Bitmap KiContrast(Bitmap b, int degree)
{
if (b == null)
{
return null;
}
if (degree < -100) degree = -100;
if (degree > 100) degree = 100;
try
{
double pixel = 0;
double contrast = (100.0 + degree) / 100.0;
contrast *= contrast;
int width = b.Width;
int height = b.Height;
BitmapData data = b.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
unsafe
{
byte* p = (byte*)data.Scan0;
int offset = data.Stride - width * 3;
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
// 處理指定位置像素的對比度
for (int i = 0; i < 3; i++)
{
pixel = ((p[i] / 255.0 - 0.5) * contrast + 0.5) * 255;
if (pixel < 0) pixel = 0;
if (pixel > 255) pixel = 255;
p[i] = (byte)pixel;
} // i
p += 3;
} // x
p += offset;
} // y
}
b.UnlockBits(data);
return b;
}
catch
{
return null;
}
}
經過以上處理,發現識別率高了很多,可是不知道什么原因對單個價格,如9,6,5 這種無法識別,而且對於3,8,0,很容易混淆,對於這種掃貨的腳本來說,價格識別率必須是100%對的。后來又去學習怎么訓練字庫,花了很多時間,最終得出一個結論,OCR訓練識別率的前提是 文字能被識別,但是識別錯了,如果連文字都識別不出,那么沒有訓練的必要了,就這樣,放棄了。
當天晚上,看了一篇別人識別網站驗證碼的文章,又看了國內的腳本開發的文字識別,看到大漠插件的字庫,是一個個像素組成的字。靈光一閃,每個價格的筆畫不同,位置不同,同樣大小的圖片,Base64值肯定不一樣啊,第二天做了實驗,證明自己的想法是對的,哪怕一個像素不對,都是不一樣的。於是寫了個腳本,把攤位里1-2000的價格都抓下來,然后處理成黑白后分割成小圖片。
/// <summary>
/// 圖像轉Base字符串
/// </summary>
/// <returns></returns>
public static string ToBaseMd5(this Bitmap img)
{
if (img == null)
return string.Empty;
else
return Convert.ToBase64String(ToByte(img));
}
做腳本嘛,最重要的截取指定區域的圖片嘛,直接上代碼。
Bitmap image = new Bitmap(26, 18);
Graphics imgGraphics = Graphics.FromImage(image);
//設置截屏區域
imgGraphics.CopyFromScreen(X, Y, 0, 0, new Size(26, 18));
image.Save(path, ImageFormat.Tiff);
以上的技術,基本上可以把識別文字的價格問題解決了,當然中途花了很多時間來做重復的事。
接下來有個問題,怎么定位價格啊,各種按鈕的位置,因此要找個參照物,簡單的說就是,截取一個參考物的圖片,然后其他元素的位置相對這個參照物進行設置。轉化成技術來說,就是一張小圖在另一張大圖里面找到位置,並返回相對坐標。嘗試了幾種方法,最終使用 AForge 這個開源項目來處理,代碼如下
/// <summary>
/// 判斷圖像是否存在
/// </summary>
/// <param name="template"></param>
/// <param name="bmp"></param>
/// <returns></returns>
public static bool ContainsImg(this Bitmap template, Bitmap bmp)
{
// create template matching algorithm's instance // (set similarity threshold to 92.1%)
ExhaustiveTemplateMatching tm = new ExhaustiveTemplateMatching(0.921f); // find all matchings with specified above similarity
TemplateMatch[] matchings = tm.ProcessImage(template, bmp); // highlight found matchings
return matchings.Length > 0;
}
/// <summary>
/// 判斷圖像是否存在另外的圖像中,並返回坐標
/// </summary>
/// <param name="template"></param>
/// <param name="bmp"></param>
/// <returns></returns>
public static Point ContainsGetPoint(this Bitmap template, Bitmap bmp)
{
// create template matching algorithm's instance // (set similarity threshold to 92.1%)
ExhaustiveTemplateMatching tm = new ExhaustiveTemplateMatching(0.921f); // find all matchings with specified above similarity
TemplateMatch[] matchings = tm.ProcessImage(template, bmp); // highlight found matchings
BitmapData data = template.LockBits(new Rectangle(0, 0, template.Width, template.Height), ImageLockMode.ReadWrite, template.PixelFormat);
Point p = new Point();
if (matchings.Length > 0)
{
Drawing.Rectangle(data, matchings[0].Rectangle, Color.White);
p = matchings[0].Rectangle.Location;
template.UnlockBits(data);
}
return p;
}
現在價格可以識別了,通過找圖,界面的各個坐標都確定了,現在就是寫模擬鼠標和鍵盤的操作了。這個網上很多,我的很簡單
對於我的游戲來說鼠標操作,就是移動和左擊
public class MouseHelper
{
[DllImport("user32.dll")]
private static extern bool SetCursorPos(int X, int Y);
[DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
private static extern void mouse_event(uint dwFlags, uint dx, uint dy, uint cButtons, UIntPtr dwExtraInfo);
/// <summary>
/// 鼠標左擊
/// </summary>
public static void LeftClick()
{
mouse_event(0x02, 0, 0, 0, UIntPtr.Zero);
mouse_event(0x04, 0, 0, 0, UIntPtr.Zero);
}
/// <summary>
/// 鼠標移動到指定的位置
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
public static void MovePoint(Point p)
{
SetCursorPos(p.X, p.Y);
}
}
鍵盤可以用C#自帶的方法 SendKeys
SendKeys.Send("輸入文本");//用於輸入文字
SendKeys.SendWait("{ENTER}");用於輸入按鍵命令
基本上就這些了,另外附上一些可能會用到的技能
找到游戲句柄
/// <summary>
/// 獲取游戲句柄
/// </summary>
/// <returns></returns>
public static int GetFFoHandle()
{
Process[] processes = Process.GetProcessesByName("進程名稱");
var p = processes.FirstOrDefault();
if (p == null)
{
return 0;
}
else
{
return p.MainWindowHandle.ToInt32();
}
}
根據句柄獲取游戲的位置
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetWindowRect(IntPtr hWnd, ref RECT lpRect);
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
根據句柄將游戲窗體移動到某個位置
/// <summary>
/// 根據句柄移動窗體
/// </summary>
/// <param name="hWnd"></param>
/// <param name="hWndInsertAfter"></param>
/// <param name="x"></param>
/// <param name="Y"></param>
/// <param name="cx"></param>
/// <param name="cy"></param>
/// <param name="wFlags"></param>
/// <returns></returns>
[DllImport("user32.dll", EntryPoint = "SetWindowPos")]
public static extern IntPtr SetWindowPos(IntPtr hWnd, int hWndInsertAfter, int x, int Y, int cx, int cy, int wFlags);
其他什么快捷鍵啊,啥的,網上一大堆就不寫了。
好了,就這些,通過以上的代碼,可以完成大部分簡單的前台腳本了,寫的比較亂,但是對於正在研究中的人,我想一定省了不少事。
