Ребята, всем привет!
Да, каюсь, аж два месяца не было от меня никаких новостей - навалилась куча проблем и работы, сменилось рабочее железо, но каждую (почти) ночь я спал и в цветных снах видел - как бы так взять да написать ещё что-нибудь.
И вот, собственно, следующая часть по работе с графикой, после прочтения которой у нас на руках будут неиллюзорные результаты. Я буду много повторяться - не обращайте внимание.
Почему статья, а не видеоурок? Потому что в основном я буду комментировать и пояснять код, в видео это так легко не сделаешь, зато потом будет видео с результатами.
Итак, что нам понадобится:
1.
[Microsoft Visual Studio]. Express, ибо бесплатная, 2010 или 2012 - без разницы. Я использую 2012. Если используете 2012 - качайте версию для desktop-разработки, ага.
2.
[DirectX SDK], на момент написания статьи - версия за июль 2010-го. Собственно, набор разработки (Lego) для тех, кто хочет что-нибудь рисовать под виндой.
!!!ВНИМАНИЕ!!!
Сначала ставим студию, потом - SDK!
!!!ВНИМАНИЕ!!!
Открываем студию, получаем вот такое окошко:
Видим манящую кнопку "New Project...", или же прожимаем Ctrl+Shift+N. В появившемся окне выбираем пустой проект на плюсах, внизу вбиваем ему какое-нибудь имя:
Больше ничего не трогаем и прожимаем Next, пока нам не покажут пустое окно студии. Всё верно, кода в нём нет. Сбоку находим "Solution Explorer", жмём на нём мышью, затем - правой кнопкой на "Source Files" и выбираем пункт "Add - New Item...":
Внизу обзываем его "main.cpp", жмём "Add", откроется пустое окно кода. Копируем туда вот это:
#include
typedef IDirect3D9* (STDMETHODCALLTYPE *DIRECT3DCREATE9)(UINT);
typedef HRESULT(WINAPI* tPresent)(LPDIRECT3DDEVICE9 pDevice, CONST RECT*, CONST RECT*, HWND, LPVOID);
typedef HRESULT(WINAPI* tReset)(LPDIRECT3DDEVICE9 pDevice, D3DPRESENT_PARAMETERS* pPresentationParameters);
typedef DWORD (STDMETHODCALLTYPE *GETPROCESSHEAPS)(DWORD, PHANDLE);
static DWORD vtableFrag9[] = { 0, 0, 0, 0 };
static DWORD* presentPtr = 0;
static DWORD* resetPtr = 0;
static DWORD offsetPresent = 0;
static DWORD offsetReset = 0;
static tPresent g_D3D9_Present = 0;
static tReset g_D3D9_Reset = 0;
LPDIRECT3DDEVICE9 npDevice;
bool indicator = 0;
void DrawIndicator(LPVOID self)
{
IDirect3DDevice9* dev = (IDirect3DDevice9*)self;
dev->BeginScene();
D3DRECT rec = { 10, 10, 30, 30 };
D3DCOLOR color = 0;
if(indicator)
{
color = D3DCOLOR_XRGB(0, 255, 0);
}
else
{
color = D3DCOLOR_XRGB(255, 0, 0);
}
dev->Clear(1, &rec, D3DCLEAR_TARGET, color, 1.0f, 0);
dev->EndScene();
}
HRESULT WINAPI hkPresent(LPDIRECT3DDEVICE9 pDevice, CONST RECT* src, CONST RECT* dest, HWND hWnd, LPVOID unused)
{
while(!npDevice)
{
npDevice = pDevice;
}
DrawIndicator(pDevice);
return g_D3D9_Present(pDevice, src, dest, hWnd, unused);
}
HRESULT WINAPI hkReset(LPDIRECT3DDEVICE9 pDevice, D3DPRESENT_PARAMETERS* pPresentationParameters)
{
return g_D3D9_Reset(pDevice, pPresentationParameters);
}
BOOL SearchHeap(HANDLE heap)
{
int vtableLenBytes = sizeof(DWORD)*4;
PROCESS_HEAP_ENTRY mem;
mem.lpData = 0;
while(HeapWalk(heap, &mem))
{
if(mem.wFlags == PROCESS_HEAP_UNCOMMITTED_RANGE) continue;
DWORD* p = (DWORD*)mem.lpData;
for(int i = 0; i < (int)(mem.cbData/sizeof(DWORD)); i++)
{
if(memcmp(p, vtableFrag9, vtableLenBytes) == 0)
{
presentPtr = p + 11;
resetPtr = p + 10;
offsetPresent = *presentPtr;
offsetReset = *resetPtr;
g_D3D9_Present = (tPresent)((DWORD*)offsetPresent);
g_D3D9_Reset = (tReset)((DWORD*)offsetReset);
break;
}
p++;
}
if(presentPtr != 0) break;
}
return(presentPtr != 0);
}
void CheckAndHookPresent9()
{
if(presentPtr != 0 && (*presentPtr) == (DWORD)hkPresent) return;
HANDLE heap = 0;
HMODULE hKern = GetModuleHandleA("kernel32.dll");
GETPROCESSHEAPS getProcessHeaps = (GETPROCESSHEAPS)GetProcAddress(hKern, "GetProcessHeaps");
if(getProcessHeaps != 0)
{
HANDLE heaps[1000];
int numHeaps = (getProcessHeaps)(1000, heaps);
for(int k = 0; k < numHeaps; k++)
{
heap = heaps[k];
if(SearchHeap(heap)) break;
}
}
else
{
heap = GetProcessHeap();
SearchHeap(heap);
}
HeapLock(heap);
if(presentPtr != 0)
{
(*presentPtr) = (DWORD)hkPresent;
}
if(resetPtr != 0)
{
(*resetPtr) = (DWORD)hkReset;
}
HeapUnlock(heap);
}
void CopyVMT9(DWORD* vtableFrag)
{
HWND hWnd = CreateWindowA("STATIC","dummy", 0, 0, 0, 0, 0, 0, 0, 0, 0);
HMODULE hD3D9 = GetModuleHandleA("d3d9");
DIRECT3DCREATE9 Direct3DCreate9 = (DIRECT3DCREATE9)GetProcAddress(hD3D9, "Direct3DCreate9");
IDirect3D9* d3d = Direct3DCreate9(D3D_SDK_VERSION);
D3DDISPLAYMODE d3ddm;
d3d->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &d3ddm);
D3DPRESENT_PARAMETERS d3dpp;
ZeroMemory(&d3dpp, sizeof(d3dpp));
d3dpp.Windowed = 1;
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
IDirect3DDevice9* d3dDevice = 0;
d3d->CreateDevice(0, D3DDEVTYPE_HAL, hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &d3dpp, &d3dDevice);
DWORD* vtablePtr = (DWORD*)(*((DWORD*)d3dDevice));
for(int i = 0; i < 4; i++)
{
vtableFrag[i] = vtablePtr[i + 6];
}
d3dDevice->Release();
d3d->Release();
DestroyWindow(hWnd);
}
PBYTE HookVTableFunction(PDWORD* dwVTable, PBYTE dwHook, INT Index)
{
DWORD dwOld = 0;
VirtualProtect((void*)((*dwVTable) + (Index*4)), 4, PAGE_EXECUTE_READWRITE, &dwOld);
PBYTE pOrig = ((PBYTE)(*dwVTable)[Index]);
(*dwVTable)[Index] = (DWORD)dwHook;
VirtualProtect((void*)((*dwVTable) + (Index*4)), 4, dwOld, &dwOld);
return pOrig;
}
bool hooked = 0;
DWORD WINAPI TF(LPVOID lpParam)
{
CopyVMT9(vtableFrag9);
CheckAndHookPresent9();
while(!npDevice && !hooked)
{
Sleep(50);
}
hooked = !hooked;
while(1)
{
Sleep(100);
HookVTableFunction((PDWORD*)npDevice, (PBYTE)hkReset, 16);
HookVTableFunction((PDWORD*)npDevice, (PBYTE)hkPresent, 17);
}
return 0;
}
DWORD WINAPI KeyboardHook(LPVOID lpParam)
{
while(1)
{
if(GetAsyncKeyState(VK_F1))
{
indicator = !indicator;
Beep(500,200);
}
Sleep(100);
}
return 0;
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
{
CreateThread(0, 0, &TF, 0, 0, 0);
CreateThread(0, 0, &KeyboardHook, 0, 0, 0);
}
case DLL_PROCESS_DETACH:
break;
}
return 1;
}
О, совсем забыл. Нам надо указать, что на выходе мы хотим не exe-файл, а dll. Меню "Project - Properties", вкладка "General", справа ищем "Configuration type: Application (.exe)", радостно меняем его на "Dynamic Library (.dll)", жмём OK.
Попробуем выяснить, собирается ли оно. Меню "Build - Build Solution". После этого внизу должно появиться что-то такое:
Значит, проект собрался и всё отлично. Если студия ругается, что не может найти заголовочный файл d3d9.h, то идём в свойства проекта, находим там вкладку "VC++ Directories", добавляем в "Include Directories" и "Library Directories" соответствующие папки из DirectX SDK, который будет лежать где-то в Program Files на диске цэ.
Итак, вроде всё хорошо, вернёмся к коду. Смотрим на самый главный метод - точку входа:
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
{
CreateThread(0, 0, &TF, 0, 0, 0);
CreateThread(0, 0, &KeyboardHook, 0, 0, 0);
}
case DLL_PROCESS_DETACH:
break;
}
return 1;
}
Так как мы пишем DLL, то и точка входа у нас называется DllMain, умеет возвращать булевую переменную (ложь или правда), ну а APIENTRY - насколько я помню, соглашение о вызове для Win32-приложений. Лучше этим не забивать голову, да. В аргументах нас интересует только один параметр, ul_reason_for_call, который определяет, что с библиотекой случилось.
В кратце, чем отличается dll от exe? Exe умеет запускаться сам, а вот dll должна быть пропихнута к уже запущенному процессу. Проверкой этого, собственно, метод и занимается - если ul_reason_for_call == DLL_PROCESS_ATTACH (присоединились к процессу), то запускаем два потока, а если DETACH (отсоединились от процесса), то ничего не делаем. После выполнения всегда возвращаем 1, то есть правду.
Если не в курсе про switch-case - почитайте, прикольная штука, я сейчас останавливаться подробно не буду.
Дык вот. Присоединились к процессу, запустили два потока. Что они, собственно, делают? Рассмотрим сначала функцию для потока KeyboardHook, потому что она покороче и попроще:
DWORD WINAPI KeyboardHook(LPVOID lpParam)
{
while(1)
{
if(GetAsyncKeyState(VK_F1))
{
indicator = !indicator;
Beep(500,200);
}
Sleep(100);
}
return 0;
}
Всё просто до безобразия. Крутимся в бесконечном цикле, в котором:
1. Если нажата F1, то
2. Меняем значение переменной indicator на противоположное (0\1)
3. Пиликаем динамиком (Beep)
4. Ждём 100 мсек
Если вдруг цикл прерывается - возвращаем 0.
...Ничего не напоминает? Правильно! Эта штука включает и выключает нашу менюшку.
Что же у нас крутится во втором потоке? Давайте посмотрим:
DWORD WINAPI TF(LPVOID lpParam)
{
CopyVMT9(vtableFrag9);
CheckAndHookPresent9();
while(!npDevice && !hooked)
{
Sleep(50);
}
hooked = !hooked;
while(1)
{
Sleep(100);
HookVTableFunction((PDWORD*)npDevice, (PBYTE)hkReset, 16);
HookVTableFunction((PDWORD*)npDevice, (PBYTE)hkPresent, 17);
}
return 0;
}
Ого, а вот тут уже интересно! Метод CopyVMT9, судя по названию, что-то делает с VMT для 9-й версии библиотеки Direct3D, а именно - вот код:
void CopyVMT9(DWORD* vtableFrag)
{
HWND hWnd = CreateWindowA("STATIC","dummy", 0, 0, 0, 0, 0, 0, 0, 0, 0);
HMODULE hD3D9 = GetModuleHandleA("d3d9");
DIRECT3DCREATE9 Direct3DCreate9 = (DIRECT3DCREATE9)GetProcAddress(hD3D9, "Direct3DCreate9");
IDirect3D9* d3d = Direct3DCreate9(D3D_SDK_VERSION);
D3DDISPLAYMODE d3ddm;
d3d->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &d3ddm);
D3DPRESENT_PARAMETERS d3dpp;
ZeroMemory(&d3dpp, sizeof(d3dpp));
d3dpp.Windowed = 1;
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
IDirect3DDevice9* d3dDevice = 0;
d3d->CreateDevice(0, D3DDEVTYPE_HAL, hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &d3dpp, &d3dDevice);
DWORD* vtablePtr = (DWORD*)(*((DWORD*)d3dDevice));
for(int i = 0; i < 4; i++)
{
vtableFrag[i] = vtablePtr[i + 6];
}
d3dDevice->Release();
d3d->Release();
DestroyWindow(hWnd);
}
Если попроще, то он создаёт пустое и невидимое окно, получает его хэндл, создаёт для него новый D3D-объект и копирует кусочек VMT этого объекта. Затем всё уничтожает, но сохраняет кусочек VMT.
Фишка тут в том, что таблицы методов (VMT) получаются для разных D3D-объектов, ибо одновременно можно использовать только один (а его создаёт и использует игра), но вот по кусочку таблицы можно найти уже созданную ранее таблицу, которая и используется игрой. По механизму действия это похоже на поиск по сигнатуре.
В общем, после выполнения этого метода у нас есть "зацепка" - сигнатура VMT для D3D-объекта, который используется игрой в данный момент. Идём дальше. Что там дальше? Ага, CheckAndHookPresent9. Вот он:
void CheckAndHookPresent9()
{
if(presentPtr != 0 && (*presentPtr) == (DWORD)hkPresent) return;
HANDLE heap = 0;
HMODULE hKern = GetModuleHandleA("kernel32.dll");
GETPROCESSHEAPS getProcessHeaps = (GETPROCESSHEAPS)GetProcAddress(hKern, "GetProcessHeaps");
if(getProcessHeaps != 0)
{
HANDLE heaps[1000];
int numHeaps = (getProcessHeaps)(1000, heaps);
for(int k = 0; k < numHeaps; k++)
{
heap = heaps[k];
if(SearchHeap(heap)) break;
}
}
else
{
heap = GetProcessHeap();
SearchHeap(heap);
}
HeapLock(heap);
if(presentPtr != 0)
{
(*presentPtr) = (DWORD)hkPresent;
}
if(resetPtr != 0)
{
(*resetPtr) = (DWORD)hkReset;
}
HeapUnlock(heap);
}
Этот метод делает довольно тривиальтую штуку - ищет в куче (Heap) процесса указатели на две нужных нам функции - Present и Reset. Ищет он при помощи метода SearchHeap, в котором нам интересна только пара деталей:
BOOL SearchHeap(HANDLE heap)
{
int vtableLenBytes = sizeof(DWORD)*4;
PROCESS_HEAP_ENTRY mem;
mem.lpData = 0;
while(HeapWalk(heap, &mem))
{
if(mem.wFlags == PROCESS_HEAP_UNCOMMITTED_RANGE) continue;
DWORD* p = (DWORD*)mem.lpData;
for(int i = 0; i < (int)(mem.cbData/sizeof(DWORD)); i++)
{
if(memcmp(p, vtableFrag9, vtableLenBytes) == 0)
{
presentPtr = p + 11;
resetPtr = p + 10;
offsetPresent = *presentPtr;
offsetReset = *resetPtr;
g_D3D9_Present = (tPresent)((DWORD*)offsetPresent);
g_D3D9_Reset = (tReset)((DWORD*)offsetReset);
break;
}
p++;
}
if(presentPtr != 0) break;
}
return(presentPtr != 0);
}
Откуда взялись смещения 10 и 11? А всё просто. VMT - это таблица указателей, в которой указатели на функции всегда расположены в одинаковом порядке. Собственно, 10 и 11 - это порядковые номера функций.
У нас есть кусочек VMT, мы идём и ищем такой же кусочек в куче процесса. Как только нашли - отсчитываем от него 10 - это будет указатель на Reset, отсчитываем 11 - указатель на Present.
И всё, нужные указатели нашли, осталось только их поменять на свои. Этим будет заниматься HookVTableFunction:
PBYTE HookVTableFunction(PDWORD* dwVTable, PBYTE dwHook, INT Index)
{
DWORD dwOld = 0;
VirtualProtect((void*)((*dwVTable) + (Index*4)), 4, PAGE_EXECUTE_READWRITE, &dwOld);
PBYTE pOrig = ((PBYTE)(*dwVTable)[Index]);
(*dwVTable)[Index] = (DWORD)dwHook;
VirtualProtect((void*)((*dwVTable) + (Index*4)), 4, dwOld, &dwOld);
return pOrig;
}
Она принимает указатель на текущее устройство D3D, указатель на новую функцию и порядковый номер функции. Вы спросите, откуда же мы возьмём устройство D3D? А всё просто. В методе CheckAndHookPresent9 мы уже заменили указатели на оригинальные функции нашими собственными. Интересовать нас будет изменённая функция hkPresent:
HRESULT WINAPI hkPresent(LPDIRECT3DDEVICE9 pDevice, CONST RECT* src, CONST RECT* dest, HWND hWnd, LPVOID unused)
{
while(!npDevice)
{
npDevice = pDevice;
}
DrawIndicator(pDevice);
return g_D3D9_Present(pDevice, src, dest, hWnd, unused);
}
Первыми же тремя строчками мы сохраняем себе адрес D3D-устройства, которое попыталось вызвать Present, но так как мы поменяли указатель в VMT - попало сюда. После этого мы можем спокойно использовать все его функции, будто это наша собственная программа, чем мы и пользуемся - вызываем DrawIndicator, который я рассмотрю чуть ниже. В общем, ясно, да? Получили устройство, установили все указатели на нужные нам.
Как выглядело раньше:
Игра - D3DDevice - VMT-Present - D3D9.DLL
Как выглядит теперь:
Игра - D3DDevice - VMT-hkPresent - hook.dll - D3D9.DLL
А вот и метод DrawIndicator, который банально рисует "менюшку":
void DrawIndicator(LPVOID self)
{
IDirect3DDevice9* dev = (IDirect3DDevice9*)self;
dev->BeginScene();
D3DRECT rec = { 10, 10, 30, 30 };
D3DCOLOR color = 0;
if(indicator)
{
color = D3DCOLOR_XRGB(0, 255, 0);
}
else
{
color = D3DCOLOR_XRGB(255, 0, 0);
}
dev->Clear(1, &rec, D3DCLEAR_TARGET, color, 1.0f, 0);
dev->EndScene();
}
Говорим, что из аргументов нам пришло устройство D3D, вызываем у него BeginScene(), чтобы порисовать, создаём квадрат 20х20 пикселей в верхнем левом углу экрана. Дальше смотрим, если indicator == true (VK_F1 и KeyboardHook все помнят?) - рисуем зелёным цветом, если false - то красным.
Рисуем мы, собственно, методом Clear() - просто очищаем выбранную область выбранным цветом, затем вызываем EndScene() и выходим.
Вот и результат, для нетерпеливых:
Оговорюсь, для запуска всего этого надо открыть игру в Cheat Engine, перейти в отладчик и найти там в меню "Tools - Inject DLL".
В принципе, вооружившись бумажкой и карандашом можно рисовать уже сейчас, но в следующем уроке (статье?) попробуем прикрутить сюда какой-нибудь шрифт. Доработок ещё уйма предстоит, этот способ работает не везде (например, у меня не работает в StarCraft II), да и много чего можно улучшить.
Такие вот дела. 200 строчек кода - а сколько веселья. :)