きっかけ
某ブラウザゲームにて、チャットで募集されるアイテムのクリック競争に負け続けて嫌気が差していました。タダでもらえるなら独り占めや~って必死なお子様が多くて困ります。僕としては、こっそり気長に楽しみたいのですよ。
必死に募集を待ち構えクリックする自分を客観視すると、配給に群がる乞食のようで僕のプライドが許しません。
かといって、募集アイテムを次々と横取りされていくのを黙ってみているわけにもいかない。。。
ブラウザゲームごときにチートはしたくはなかったのですが、久しぶりに、ちょっとえげつない行為をしてみたくなりましたw
どうする?
winpcapで覗いてみると、ブラウザゲームといえども普通にTCPを使用しており、無防備なことに暗号化はしていませんでした。
(おそらく、ActionScriptのフレームワークをそのまま使っていて、やり取りするデータはだらだらと無駄が多く、圧縮にはzlibをつかっていました。)
正攻法としてはブラウザごと乗っ取るのがいいんでしょうが、そこまでやるのもめんどくさいですし、Pスキルを競うようなゲームでもないので、お手軽に画面キャプチャして、文字判定してクリックする方法を取ることにしました。
画面キャプチャをお手軽に
確かDWM以前と以後とで画面キャプチャの方法がちょっと変わったんだよなぁと調べていたのですが、キャプチャのモニタリング表示でMagnificationAPIを使用してみることにしました。
MagnificationAPI、Microsoftサイトの文献と違って、Windows10でそれなりにバージョンアップしてるんですよね。
Microsoftサイトだと拡大だけで縮小はできないと書いてあるのですが、普通に縮小できますw
MagInitialize()、MagUninitialize()で挟んでおいて、、、
# include <magnification.h>
# pragma comment(lib, "magnification.lib")
# pragma comment(linker, "/subsystem:\"WINDOWS\"")
# pragma comment(linker, "/entry:\"WinMainCRTStartup\"")
int __stdcall WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int)
{
MagInitialize();
Initialize();
RunMessageLoop();
Uninitilize();
MagUninitialize();
return 0;
}
ウィンドウクラス名がWC_MAGNIFIERの子ウィンドウを作成し、そのウインドウに倍率を指定します。
MAGTRANSFORM mat{};
mat.v[0][0] = 1.f;//MAGFACTOR;
mat.v[1][1] = 1.f;//MAGFACTOR;
mat.v[2][2] = 1.f;
MagSetWindowTransform(hMagCap, &mat);
MagSetWindowSource()を実行すると画像キャプチャして指定ウィンドウの画像が更新されるので、メッセージループに組み込みます。
キャプチャ位置として、デスクトップ画面の左上座標を指定します。(ウィンドウサイズは、表示ウィンドウのサイズと同じになります。)
for(;;){
if(PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)!=0){
if(msg.message==WM_QUIT) break;
TranslateMessage(&msg);
DispatchMessage(&msg);
}
if(SystemReady==true){
RECT cappos_src{};
cappos_src.left=1000;
cappos_src.top=100;
MagSetWindowSource(hMagCap, cappos_src);
}
_mm_pause();
}
画像データの取得は、今まで通り(Win32API的にも^^;)BitBlt()で行います。
あらかじめCreateDIBitmap()でバッファ(hbmp)を用意しておき、画面全体のキャプチャ(スクリーンキャプチャ)から切り出してコピーします。
(昔は、画面全体でなく個別のウィンドウのDCからコピーできたような気がするんですが。。。)
RECT rc{}; GetWindowRect(hMagCap, &rc);
HDC hdc=GetDC(NULL);
HDC hmemdc=CreateCompatibleDC(hdc);
HBITMAP holdbmp=(HBITMAP)SelectObject(hmemdc, hbmp);
BitBlt(hmemdc,
0, 0, rc.right-rc.left, rc.bottom-rc.top,
hdc,
rc.left, rc.top,
SRCCOPY);
SelectObject(hmemdc, holdbmp);
DeleteDC(hmemdc);
ReleaseDC(NULL, hdc);
で取り込んだ画像データから文字認識して判定、クリック動作をブラウザに送り込む、で完成かな。
下手に自前でキャプチャモニタリングまでコーディングしようとするとめんどくさいですからね。
MagnificationAPIでマウスカーソル有無の表示、キャプチャするウィンドウ選択が簡単にできるので、この方法を採用しました。
(追記) 実戦
文字認識は、画像処理的にやるまでもないな、と思い、2値化後に対象画素ブロックをバイナリーシリアライズして、std::searchであらかじめ記憶しておいた辞書データと一致する位置を探すようにしました。
あとは対象となるウィンドウハンドルを取得して、検出位置にマウスカーソルを移動させ、ターゲットの入力キューを横取りして、クリックするだけ
(下図)
POINT orig_pt; GetCursorPos(&orig_pt);
SetCursorPos(cappos_src.left+dist+7, cappos_src.top+158);
// ShowCursor(FALSE);
if(hwnd_target!=NULL){
HWND hwnd_from=hwnd_target;
HWND hwnd_to=hDlg;
DWORD fid=GetWindowThreadProcessId(hwnd_from, NULL);
DWORD tid=GetWindowThreadProcessId(hwnd_to, NULL);
AttachThreadInput(fid, tid, TRUE);
{
INPUT Inputs[2]={0};
Inputs[0].type=INPUT_MOUSE;
Inputs[0].mi.dwFlags=MOUSEEVENTF_LEFTDOWN;
Inputs[1].type=INPUT_MOUSE;
Inputs[1].mi.dwFlags=MOUSEEVENTF_LEFTUP;
SendInput(2, Inputs, sizeof(INPUT));
}
AttachThreadInput(fid, tid, FALSE);
// ShowCursor(TRUE);
// SetCursorPos(orig_pt.x, orig_pt.y);
結果ですが、クリック競争楽勝^^;そりゃ文字認識をタスクスレッドでぶん回してんだから当然かw
ただぶん回し過ぎなのか、ハングることが度々あったので調整が必要です。
この手のチートで注意しないといけないのは、調子に乗ってやりすぎないことですね。競争相手を生かさず殺さず、敢えて負けてみたりねw
経験上、チートツールの開発者というのは、大概はそういう意識を持ってやっているのでいいのですが、ツールを一般配布でもして乞食に与えると最悪の結果を生んでしまいます。ゲームチートなんてものは技術あるものがこっそりやるべきなんです。
(追記2) ゲーム用ブラウザ
私は、ゲーム用のブラウザとしてMaxthonNitroを使用しています。中華ソフトなんでちょっと嫌なんですが、まぁ仕方ない><
ブラウザゲームも年末のフラッシュ対応終了に伴って消えていくんでしょうね。。。
MaxthonNitroですが、ブラウザゲームに必要な機能だけを残し、余分な機能を削って高速化しているのが素晴らしいです。
機能的特徴として一番大きいのが、
・タブブラウザ、だけど1プロセス1ウィンドウに固定
じゃないですかね。タブを切り替えると、切り替えられたタブがサスペンド状態になるんすよ。その分、表示タブにリソース全振りしてくれるんでゲーム用には好都合です。
なお、タブを引っ張り出して複数ウィンドウの表示にすることはできません。あくまで1プロセス1ウィンドウ、表示タブだけ動作、です。
それでも複数起動、複数ウィンドウ表示にしたい場面はあるわけで、そうしたい場合どうするかというと、
インストールフォルダをまるごとコピーして、コピーした実行ファイルをコピー元と同時に起動する
です。これ気づくのに半日かかりましたw
(追記3) 複垢操作
さらに調子に乗ってサクッと作ってみた。
MaxthonNitroを2つ起動して重ねて表示し、そのキャプチャ画面の縮小を上下に表示するようにしてみました。
それぞれの領域でのマウスの左クリック情報は、対応するウィンドウに送られるようにしたので、複垢操作も楽ちん!
ブラウザゲームなんて左クリックしか使わないからねぇ。。。
ただMagnificationAPIだと縮小品質が荒いんです><
内部動作的にDirectXの転送オプションの品質指定で簡単にできるはずなんですけどね。
きれいに表示させたかったらDWMサムネイルとか別のAPIを使ったほうがいいのかもしれないですが、まぁめんどくさいしこれでいいや。