- Создание ASI-плагина с нуля
- Хуки – что это такое и как с ними работать
- Безопасная инициализация и работа с SAMP
- Работа с рендером и Directx9
- Обработка событий окна + ImGui
При использовании на других ресурсах необходимо указание авторства и ссылки на оригинальную темы!
Все действия производились на Visual Studio 2019 с параметром
/std:c++17
, в других версиях интерфейс может отличаться.И так, начнем:
Создаем новый проект, настраиваем его, добавляем библиотеку хуков(буду показывать на примере своих хуков, как настроить проект и подключить хуки можете посмотреть в гайдах [1] и [3])
В свойствах проекта, в вкладке общие, стандарт языка C++ ставим /std:c++17
Устанавливаем Directx9 SDK на наш пк и подключаем к проекту:
Скачиваем установщик, производим полную установку, перезагружаем наш пк(желательно)
В панели меню сверху жмем Проект, и выпадающем меню выбираем пункт Свойства: $ProjectName
Сверху, в выпадающем меню в открывшемся диалоге выбираем Конфигурация -> Все конфигурации.
Директории VC++(VC++ Directories) -> Директории включения файлов(Include directories) -> добавляем в конец
$(DXSDK_DIR)\Include\;
(точка с запятой в конце обязательна)Директории VC++(VC++ Directories) -> Директории библиотек(Library Directories) -> добавляем в конец
$(DXSDK_DIR)\Lib\x86;
(точка с запятой в конце обязательна)Компоновщик(Linker) -> Ввод(Input) -> Дополнительные файлы зависимостей(Additional Dependencies) -> добавляем
d3d9.lib;d3dx9.lib;
(точки с запятой опять же обязательны)Подключаем ImGui к нашему проекту:
ImGui можно подключать двумя путями.
Самый простой -
vcpkg install imgui
Второй способ:
Скачиваем ImGui из репозитория и распаковываем все cpp и h файлы из корневой папки репозитория в папку нашего проекта. Также из папки backends берем файлы
- imgui_impl_win32.cpp
- imgui_impl_win32.h
- imgui_impl_dx9.h
- imgui_impl_dx9.cpp
Теперь перейдем к написанию кода:
Хукать directx в gta можно двумя путями:
- Хукать таблицу виртуальных методов
- Хукать функцию в библиотеке d3d9.dll
Покажу 2 и 3 способ, 1 способ делается на основе 3
И так, сначала расскажу что такое таблица виртуальных методов. Таблица виртуальных методов используется для разрешения виртуальных вызовов функций в коде. Благодаря ней выполняется получение и вызов нужной функции во время выполнения.
Обычно я оборачиваю все это в классы, но так как у нас базовый пример - буду показывать на примере свободных функций.
Чтобы хукать directx перед созданием девайса игрой, нам нужно искать виртуальную таблицу по сигнатуре в
d3d9.dll
После ее нахождения мы возьмем оттуда адрес на функцию, и уже на самой функции будем ставить хук
C++:
#include "d3d9.h"
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;
}
Также напишем вспомогательную функцию для получения указателя функции в массиве:
C++:
void* get_function_address(int VTableIndex) {
return (*reinterpret_cast<void***>(find_device(0x128000)))[VTableIndex];
}
Теперь у нас все есть для хука, и можем просто создавать хук:
C++:
#include "kthook/kthook.hpp"
// Сигнатуры функций
using PresentSignature = HRESULT(__stdcall*)(IDirect3DDevice9*, const RECT*, const RECT*, HWND, const RGNDATA*);
using ResetSignature = HRESULT(__stdcall*)(IDirect3DDevice9*, D3DPRESENT_PARAMETERS*);
// Создаем хуки и сразу же инициализируем их на адреса в d3d9.dll
kthook::kthook_signal<PresentSignature> present_hook{ get_function_address(17) };
kthook::kthook_signal<ResetSignature> reset_hook{ get_function_address(16) };
Создаем функции коллбэки для хуков(пока пустышки):
C++:
std::optional<HRESULT> on_present(const decltype(present_hook)& hook, IDirect3DDevice9* device_ptr, const RECT*, const RECT*, HWND, const RGNDATA*) {
return std::nullopt; // не нужно прерывать выполнение
}
std::optional<HRESULT> on_lost(const decltype(reset_hook)& hook, IDirect3DDevice9* device_ptr, D3DPRESENT_PARAMETERS* parameters) {
return std::nullopt; // не нужно прерывать выполнение
}
void on_reset(const decltype(reset_hook)& hook, HRESULT& return_value, IDirect3DDevice9* device_ptr, D3DPRESENT_PARAMETERS* parameters) {
}
Сейчас вы наверное скажете "кинч че за хуйню ты тут написал какие деклтайпы и опшионалы ваще"
kthook пробрасывает состояние хука внутрь коллбэка. Чтобы вывести правильный тип для аргумента hook - мы используем decltype, который выдает тип переменной хука и подставит его.
Также можно делать вот так:
C++:
template<typename HookT>
void on_reset(const HookT& hook, HRESULT& return_value, IDirect3DDevice9* device_ptr, D3DPRESENT_PARAMETERS* parameters) {
}
Вернусь к рассказу. Про decltype рассказал, теперь про
std::optional
.std::optional
это специальный библиотечный тип, который позволяет делать объекты в которых либо есть значение, либо его нет. std::nullopt это пустой std::optional.Чтобы kthook не прерывал выполнение оригинальной функции нужно возвращать пустой std::optional
Теперь будем разбираться с Directx.
on_present - коллбэк который будет использоваться для обработки Present. Если не углубляться в подробности - функции вызывается каждый кадр для отрисовки.
on_lost - коллбэк который будет обрабатывать состояние перед Reset. Вызывается при сбросе девайса(например при разворачивании игры)
on_reset - коллбэк который будет обрабатывать после Reset. В нашем случае не понадобится, но показать думаю стоило
Перед отрисовкой нам нужно инициализировать ImGui, будем это делать в present:
C++:
#include "imgui.h"
#include "imgui_impl_dx9.h"
#include "imgui_impl_win32.h"
static bool ImGui_inited = false;
if (!ImGui_inited) {
// Создаем имгуи контекст
ImGui::CreateContext();
// Инициализируем OS зависимую часть(обрабатывает открытие шрифтов, обработку нажатия клавиш и т.д.)
ImGui_ImplWin32_Init(**reinterpret_cast<HWND**>(0xC17054));
// Инициализируем render framework зависимую часть(обрабатывает отрисовку на экране, создание текстур шрифтов и т.д.)
ImGui_ImplDX9_Init(device_ptr);
ImGui_inited = true;
}
В коде выше я использовать магическую константу
0xC17054
Это одно из полей информации о движке игры. Оно хранит хендл окна необходимый для инициализации. Но дергать напрямую по адресу не стоит. Я так делаю для упрощения примера :D
Также для корректной работы всего Directx нам нужно сбрасывать ресурсы, которые создает ImGui в процессе своей работы(текстуры шрифтов например).
Это делать нужно во время сброса девайса, т.е. в on_reset:
C++:
ImGui_ImplDX9_InvalidateDeviceObjects();
Теперь можно рисовать на экране.
Для этого в on_present будем инициализировать ImGui для отрисовки одного кадра, рисовать все что нам нужно, и завершать кадр, и отдавать команду на рендер:
C++:
// Инициализируем render часть для нового кадра
ImGui_ImplDX9_NewFrame();
// Инициализируем OS часть для нового кадра
ImGui_ImplWin32_NewFrame();
// Создаем новый кадр внутри ImGui
ImGui::NewFrame();
// тут будем рисовать
// Завершаем кадр ImGui
ImGui::EndFrame();
// Рендерим ImGuiв внутренний буффер
ImGui::Render();
// Отдаем Directx внутренний буффер на рендер
ImGui_ImplDX9_RenderDrawData(ImGui::GetDrawData());
Но при выгрузке плагина нужно освобождать ресурсы ImGui, поэтому в DLL_PROCESS_DETACH добавляем:
C++:
case DLL_PROCESS_DETACH:
ImGui_ImplDX9_Shutdown();
ImGui_ImplWin32_Shutdown();
ImGui::DestroyContext();
break;
Рисовать примитивы будет через дравлисты ImGui, т.к. это очень сильно оптимизированный и удобный вариант, нежели всякие костыльные самодельные функции
В этом гайде я буду выводить красный текст на экране и квадрат
C++:
// получаем дравлист
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());
Теперь остается только подключить коллбэки к хукам:
C++:
case DLL_PROCESS_ATTACH: {
DisableThreadLibraryCalls(hModule);
present_hook.before += on_present;
reset_hook.before += on_lost;
reset_hook.after += on_reset;
break;
}
Скомпилировать плагин и увидеть результат:
Теперь расскажу про второй способ.
Т.к. таблица виртуальных методов это просто массив из указателей (
void* vtbl[];
, то мы можем просто изменить указатель в ней на свой.Сам указатель на объект
IDirect3DDevice9
лежит по адресу 0xC97C28
Т.к. функция будет вызываться вместо оригинальной, нам нужно сделать их такими же как оригинал:
C++:
HRESULT __stdcall on_present(IDirect3DDevice9* device_ptr, const RECT*, const RECT*, HWND, const RGNDATA*);
HRESULT __stdcall on_reset(IDirect3DDevice9* device_ptr, D3DPRESENT_PARAMETERS* parameters);
При этом on_lost будет до вызова prev_reset_ptr(о нем чуть ниже), а on_reset - после вызова и проверки результата вызова на D3D_OK
Чтобы изменить указатель в таблице, напишем вспомогательную функцию:
C++:
void* set_vtable_pointer(void* class_ptr, std::size_t index, void* value) {
void** vtbl = *reinterpret_cast<void***>(class_ptr);
void* prev = vtbl[index];
vtbl[index] = value;
return prev;
}
Теперь мы можем подменить указатель на метод:
C++:
auto device_ptr = *reinterpret_cast<IDirect3DDevice9**>(0xC97C28);
void* prev_present_ptr = set_vtable_pointer(device_ptr, 17, &on_present);
void* prev_reset_ptr = set_vtable_pointer(device_ptr, 16, &on_reset);
При отгрузке плагина нужно возвращать прошлые значения указателей, иначе ваш крашнет.
Также стоит помнишь, что по адресу
0xC97C28
ничего не будет вплоть до создания окна игры
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"
// Сигнатуры функций
using PresentSignature = HRESULT(__stdcall*)(IDirect3DDevice9*, const RECT*, const RECT*, HWND, const RGNDATA*);
using ResetSignature = HRESULT(__stdcall*)(IDirect3DDevice9*, D3DPRESENT_PARAMETERS*);
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];
}
// Создаем хуки и сразу же инициализируем их на адреса в d3d9.dll
kthook::kthook_signal<PresentSignature> present_hook{ get_function_address(17) };
kthook::kthook_signal<ResetSignature> reset_hook{ get_function_address(16) };
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(**reinterpret_cast<HWND**>(0xC17054));
// Инициализируем render framework зависимую часть(обрабатывает отрисовку на экране, создание текстур шрифтов и т.д.)
ImGui_ImplDX9_Init(device_ptr);
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
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;
}
Последнее редактирование: