- Создание ASI-плагина с нуля
- Хуки – что это такое и как с ними работать
- Безопасная инициализация и работа с SAMP
- Работа с рендером и Directx9
- Обработка событий окна + ImGui
При использовании на других ресурсах необходимо указание авторства и ссылки на оригинальную темы!
Все действия производились на Visual Studio 2019 с параметром /std:c++17, в других версиях интерфейс может отличаться.
И так, начнем:
Для этого гайда нам не нужно никаким образом настраивать проект и что-то в него добавлять. Можно воспользоваться готовым из гайда [4]
В Windows существует множество различных возможностей по работе с окнами(на то она и Windows). И чтобы все было максимально гибко, Windows может отдавать высокоуровневые события окон, и низкоуровневые. Про низкоуровневые события сегодня и пойдет речь.
Все события окна Windows присылает в коллбэк WindowProc. Сюда входят нажатия мышкой, нажатиян на клавиатуру, перемещение окна, изменение его размера, сворачивание, разворачивание и еще куча других событий(около 2000, если не ошибаюсь).
У каждого приложения должен быть создан коллбэк на события окна, иначе вы не сможете создать окно. Т.к. мы работаем внутри готового окна, нам нужно перехватывать уже существующий обработчик событий.
Делать это можно несколькими способами:
- Штатными средствами Windows
- Перехват самого первого коллбэка(нужно знать его адрес)
- Перехват самого последнего зарегистрированного коллбэка
Начнем с того, что для всех способов, кроме 2 нам нужен хендл окна. В общем случае нам придется получать его через "костыли" поиском по имени окна(FindWindow)
Начнем с того, что нам нужно получить HWND нашего окна. В GTA:SA его можно вытащить из внутренней структуры движка игры. Но просто вытаскивать HWND из структуры игры не всегда хорошая затея, особенно если мы хотим перехватывать события еще до полного запуска игры. Поэтому мы перехватим функцию, создающую окно игры и будем вытаскивать HWND оттуда.
Перейдем к созданию хука
Чтобы не гонять в холостую код, мы сделаем проверку на то, существует ли уже окно игры, и если оно уже создано - будем брать hwnd оттуда.
(На самом деле показанное - почти бесполезно. Но я все же посчитал нужным показать это, лишним точно не будет)
Также можно перехватывать CreateWindow, но там чуть больше заморочек, но зато способ универсальный, и будет работать везде
C++:
// Сигнатура
using InitGameInstance = HWND(__cdecl*)(HINSTANCE);
kthook::kthook_signal<InitGameInstance> game_instance_init_hook{ 0x745560 };
HWND game_hwnd = []() {
// Указатель на HWND внутри движка игры
HWND* hwnd_ptr = *reinterpret_cast<HWND**>(0xC17054);
if (hwnd_ptr != nullptr) {
return *hwnd_ptr;
}
else {
// Ставим коллбэк после выполенение оригинальной функции, т.к. нам нужен ее возврат
game_instance_init_hook.after += [](const auto& hook, HWND& return_value, HINSTANCE inst) {
// присваиваем нашей переменной значение, что вернула нам функция
game_hwnd = return_value;
};
return HWND(0);
}
}();
Наверное вы спросите, что произошло :D
game_hwnd - глобальная переменная. Все глобальные переменные инициализируются до перехода к основной функции программы(DllMain в нашем случае). А чтобы при инициализации переменной выполнился наш код с условиями - мы создаем лямбду, и сразу же ее вызываем, в итоге результат вызова нашей лямбды будет записан в переменную.
Теперь game_hwnd сама инициализируется, как только окно игры будет создано.
И уже сейчас мы можем перехватывать обработчик событий окна.
Начнем с самого простого способа - 3
Перехватывать текущий обработчик событий можно где угодно, но я буду делать это внутри хука Present. Это можно было бы сделать даже внутри game_instance_init_hook.
Чтобы получить текущий обработчик - воспользуемся функцией GetWindowLongPtr.
И так, создадим хук и воспользуемся сигнатурой, объявленной внутри WINAPI:
C++:
kthook::kthook_simple<WNDPROC> wndproc_hook{};
C++:
HRESULT __stdcall on_wndproc(const decltype(wndproc_hook)& hook, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
// вызываем оригинал
return hook.get_trampoline()(hwnd, uMsg, wParam, lParam);
}
C++:
auto latest_wndproc_ptr = GetWindowLongPtrW(game_hwnd, GWLP_WNDPROC);
wndproc_hook.set_dest(latest_wndproc_ptr);
wndproc_hook.set_cb(&on_wndproc);
wndproc_hook.install();
Перехват функции игры примерно такой же, и т.к. мне лень писать 1 строку кода - попробуйте сделать это сами, адрес игровой функции -
0x747EB0
Также нужно учитывать, что функция игры будет самой последней в цепочке, и поэтому если кто-то зарегистрирует свой обработчик позже - вы можете не получить событие.
Ну и последний способ - зарегистрировать свой обработчик средствами Windows.
kthook::kthook_simple<WNDPROC> wndproc_hook{};
- Эту строку и все связанные с ней нужно будет удалить.Добавляем переменную для хранения прошлого обработчика.
C++:
WNDPROC old_wndproc{};
HRESULT __stdcall on_wndproc(const decltype(wndproc_hook)& hook, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
// вызываем оригинал
return CallWindowProcA(old_wndproc, hwnd, uMsg, wParam, lParam);
}
Ну и теперь вместо кода с установкой хука на основе kthook, пишем что-то такое:
C++:
old_wndproc = reinterpret_cast<WNDPROC>(SetWindowLongPtrA(game_hwnd, GWLP_WNDPROC, reinterpret_cast<LONG>(&on_wndproc)));
Внимательнее на строке возврата, код ниже показан на примере первого примера(да, тавтология)
Теперь попробуем обработать нажатие клавиши и вывести сообщение в чат.
C++:
HRESULT __stdcall on_wndproc(const decltype(wndproc_hook)& hook, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) {
// если событие - нажатие клавиши
case WM_KEYDOWN: {
// если кнопка F11 и клавиша не повторялась до этого(нажата в первый раз)
if (wParam == VK_F11 && (HIWORD(lParam) & KF_REPEAT) != KF_REPEAT) {
sampapi::v037r3::RefChat()->AddChatMessage("", 0xFFFFFFFF, "Привет из WNDPROC!");
}
break;
}
}
// вызываем оригинал
return hook.get_trampoline()(hwnd, uMsg, wParam, lParam);
}
Компилируем, запускаем, жмякаем F11 и видим сообщение в чате:
Но у нас все еще осталась проблема - ImGui не будет обрабатывать наши нажатия.
Чтобы это сделать - нужно объявить обработчик ImGui, добавив где-нибудь такую строку:
C++:
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
Ну, а теперь, чтобы все верно работало, нам нужно вызвать эту функцию внутри обработчика событий окон, и блокировать дальнейшую обработку клавиш, если ImGui хочет этого. Пример показан только ради показа, как это делать, в случае когда будут окна зависящие от булевой переменной - нужно будет обернуть это в
if (window_opened)
C++:
ImGui_ImplWin32_WndProcHandler(hwnd, uMsg, wParam, lParam);
auto& io = ImGui::GetIO();
if (io.WantCaptureKeyboard || io.WantCaptureMouse) {
return 1;
}
Ну и теперь можем сделать тестовый пример:
C++:
ImGui::Begin("Window");
if (ImGui::Button("Click me!")) {
sampapi::v037r3::RefChat()->AddChatMessage("", 0xFFFFFFFF, "Привет из ImGui!");
}
ImGui::End();
Конпелируем, запускаем, жмякаем по кнопочкам, и видим что все работает:
Но осталась одна проблема - ImGui расчитан на работу с wchar_t, который используется для UTF16 на windows, а обработчик событий нашего окна работает в CP_ACP кодировке. Чтобы это исправить, нам нужно конвертировать CP_ACP в UTF16 перед передачей в ImGui чтобы не видеть каракули при вводе.
Для этого, перед вызовом ImGui_ImplWin32_WndProcHandler, добавим такой код:
C++:
if (uMsg == WM_CHAR) {
wchar_t wch;
MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, reinterpret_cast<char*>(&wParam), 1, &wch, 1);
wParam = wch;
}
C++:
#include <windows.h>
#include <string>
#include "d3d9.h"
#include "kthook/kthook.hpp"
#include "imgui.h"
#include "imgui_impl_dx9.h"
#include "imgui_impl_win32.h"
#include "sampapi/CChat.h"
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
// Сигнатуры функций
using PresentSignature = HRESULT(__stdcall*)(IDirect3DDevice9*, const RECT*, const RECT*, HWND, const RGNDATA*);
using ResetSignature = HRESULT(__stdcall*)(IDirect3DDevice9*, D3DPRESENT_PARAMETERS*);
using InitGameInstance = HWND(__cdecl*)(HINSTANCE);
std::uintptr_t find_device(std::uint32_t Len) {
static std::uintptr_t base = [](std::size_t Len) {
std::string path_to(MAX_PATH, '\0');
if (auto size = GetSystemDirectoryA(path_to.data(), MAX_PATH)) {
path_to.resize(size);
path_to += "\\d3d9.dll";
std::uintptr_t dwObjBase = reinterpret_cast<std::uintptr_t>(LoadLibraryA(path_to.c_str()));
while (dwObjBase++ < dwObjBase + Len) {
if (*reinterpret_cast<std::uint16_t*>(dwObjBase + 0x00) == 0x06C7 &&
*reinterpret_cast<std::uint16_t*>(dwObjBase + 0x06) == 0x8689 &&
*reinterpret_cast<std::uint16_t*>(dwObjBase + 0x0C) == 0x8689) {
dwObjBase += 2;
break;
}
}
return dwObjBase;
}
return std::uintptr_t(0);
}(Len);
return base;
}
void* get_function_address(int VTableIndex) {
return (*reinterpret_cast<void***>(find_device(0x128000)))[VTableIndex];
}
kthook::kthook_signal<InitGameInstance> game_instance_init_hook{ 0x745560 };
HWND game_hwnd = []() {
// Указатель на HWND внутри движка игры
HWND* hwnd_ptr = *reinterpret_cast<HWND**>(0xC17054);
if (hwnd_ptr != nullptr) {
return *hwnd_ptr;
}
else {
// Ставим коллбэк после выполенение оригинальной функции, т.к. нам нужен ее возврат
game_instance_init_hook.after += [](const auto& hook, HWND& return_value, HINSTANCE inst) {
// присваиваем нашей переменной значение, что вернула нам функция
game_hwnd = return_value;
};
return HWND(0);
}
}();
// Создаем хуки и сразу же инициализируем их на адреса в d3d9.dll
kthook::kthook_signal<PresentSignature> present_hook{ get_function_address(17) };
kthook::kthook_signal<ResetSignature> reset_hook{ get_function_address(16) };
kthook::kthook_simple<WNDPROC> wndproc_hook{};
HRESULT __stdcall on_wndproc(const decltype(wndproc_hook)& hook, HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) {
// если событие - нажатие клавиши
case WM_KEYDOWN: {
// если кнопка F11 и клавиша не повторялась до этого(нажата в первый раз)
if (wParam == VK_F11 && (HIWORD(lParam) & KF_REPEAT) != KF_REPEAT) {
sampapi::v037r3::RefChat()->AddChatMessage("", 0xFFFFFFFF, "Привет из WNDPROC!");
}
break;
}
}
if (uMsg == WM_CHAR) {
wchar_t wch;
MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, reinterpret_cast<char*>(&wParam), 1, &wch, 1);
wParam = wch;
}
ImGui_ImplWin32_WndProcHandler(hwnd, uMsg, wParam, lParam);
auto& io = ImGui::GetIO();
if (io.WantCaptureKeyboard || io.WantCaptureMouse) {
return 1;
}
// вызываем оригинал
return hook.get_trampoline()(hwnd, uMsg, wParam, lParam);
}
std::optional<HRESULT> on_present(const decltype(present_hook)& hook, IDirect3DDevice9* device_ptr, const RECT*, const RECT*, HWND, const RGNDATA*) {
static bool ImGui_inited = false;
if (!ImGui_inited) {
// Создаем имгуи контекст
ImGui::CreateContext();
// Инициализируем OS зависимую часть(обрабатывает открытие шрифтов, обработку нажатия клавиш и т.д.)
ImGui_ImplWin32_Init(game_hwnd);
// Инициализируем render framework зависимую часть(обрабатывает отрисовку на экране, создание текстур шрифтов и т.д.)
ImGui_ImplDX9_Init(device_ptr);
auto latest_wndproc_ptr = GetWindowLongPtrA(game_hwnd, GWLP_WNDPROC);
wndproc_hook.set_dest(latest_wndproc_ptr);
wndproc_hook.set_cb(&on_wndproc);
wndproc_hook.install();
ImGui_inited = true;
}
// Инициализируем render часть для нового кадра
ImGui_ImplDX9_NewFrame();
// Инициализируем OS часть для нового кадра
ImGui_ImplWin32_NewFrame();
// Создаем новый кадр внутри ImGui
ImGui::NewFrame();
// получаем дравлист
auto drawlist = ImGui::GetBackgroundDrawList();
// Вычисляем размер текста
std::string text{ "Hello from kin4!" };
ImVec2 text_size = ImGui::CalcTextSize(text.c_str());
// Рисуем прямоугольник с от 0;0 до text_size + 20; text_size + 20 белого цвета и закруглением 5 пикселей
drawlist->AddRectFilled(ImVec2(0, 0), ImVec2(text_size.x + 20.0f, text_size.y + 20.0f), 0xFFFFFFFF, 5.0f);
// Вычисляем позицию текста
ImVec2 pos{ 10.0f, 10.0f };
ImVec4 text_color{ 1.0f, 0.0f, 0.0f, 1.0f };
// Рисуем текст
drawlist->AddText(pos, ImGui::GetColorU32(text_color), text.c_str());
ImGui::Begin("Window");
if (ImGui::Button("Click me!")) {
sampapi::v037r3::RefChat()->AddChatMessage("", 0xFFFFFFFF, "Привет из ImGui!");
}
ImGui::End();
// Завершаем кадр ImGui
ImGui::EndFrame();
// Рендерим ImGuiв внутренний буффер
ImGui::Render();
// Отдаем Directx внутренний буффер на рендер
ImGui_ImplDX9_RenderDrawData(ImGui::GetDrawData());
return std::nullopt; // не нужно прерывать выполнение
}
std::optional<HRESULT> on_lost(const decltype(reset_hook)& hook, IDirect3DDevice9* device_ptr, D3DPRESENT_PARAMETERS* parameters) {
ImGui_ImplDX9_InvalidateDeviceObjects();
return std::nullopt; // не нужно прерывать выполнение
}
void on_reset(const decltype(reset_hook)& hook, HRESULT& return_value, IDirect3DDevice9* device_ptr, D3DPRESENT_PARAMETERS* parameters) {
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH: {
DisableThreadLibraryCalls(hModule);
present_hook.before += on_present;
reset_hook.before += on_lost;
reset_hook.after += on_reset;
break;
}
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}